Socket编程——基于TCP实现自己的通信协议

时间:2022-07-20 10:23:37

假如我们要做一个C/S型的程序设计,服务端和客户端使用TCP通信,这时就需要在TCP协议之上,选择一个合适的应用层协议,如果不喜欢已有的协议,那就需要自己去实现一个协议规程,现在我们就要去完成一个图1所示的协议。

 Socket编程——基于TCP实现自己的通信协议

图1

1.定义传输的消息格式

         该协议基本类似于简单邮件传输协议SMTP,不过我们需要做一下改变:信息传输不局限于ASCII码,要能够传输任何对象,这里采用了传输实体类,将其序列化为二进制流进行传输,代码1是定义的消息MessageEntity,注意它使用了Serializable标记,否则无法对它进行序列化,另外,传输的数据也必须被Serializable标记。

[csharp] view plain copy print?
  1. [Serializable]  
  2.     public class MessageEntity  
  3.     {  
  4.         private CommandType _command;  
  5.         /// <summary>  
  6.         /// 命令类型  
  7.         /// </summary>  
  8.         public CommandType Command  
  9.         {  
  10.             get { return _command; }  
  11.             set { _command = value; }  
  12.         }  
  13.         private object _data;  
  14.         /// <summary>  
  15.         /// 消息数据  
  16.         /// </summary>  
  17.         public object Data  
  18.         {  
  19.             get { return _data; }  
  20.             set { _data = value; }  
  21.         }  
  22.         public MessageEntity(CommandType command, object data)  
  23.         {  
  24.             this._command = command;  
  25.             this._data = data;  
  26.         }  
  27.   
  28.         public enum CommandType  
  29.         {  
  30.             OK,  
  31.             #region Server's Command  
  32.             /// <summary>  
  33.             /// 服务器就绪  
  34.             /// </summary>  
  35.             ServerReady,  
  36.             /// <summary>  
  37.             /// 允许客户断开  
  38.             /// </summary>  
  39.             AgreeDisconnect,  
  40.             #endregion  
  41.             #region Client's Command  
  42.             /// <summary>  
  43.             /// 呼叫服务器  
  44.             /// </summary>  
  45.             HelloServer,  
  46.             /// <summary>  
  47.             /// 数据消息  
  48.             /// </summary>  
  49.             Data,  
  50.             /// <summary>  
  51.             /// 请求退出  
  52.             /// </summary>  
  53.             Quit  
  54.             #endregion  
  55.         }  
  56.     }  

代码1

2.实现服务器

         现在知道了通信的协议流程和通信消息体,那么就可以开始编程了,首先完成服务器端的程序。对服务器的要求是能允许多客户连接。那么,每次当服务器接受一个连接后,就新开一个线程,由该线程继续去处理和客户端的通信事宜,而服务器回到监听状态,继续等待新的连接。如代码2所示。

[csharp] view plain copy print?
  1. Socket client = serverSocket.Accept();  
  2. ThreadPool.QueueUserWorkItem(ClientProcess, client);  
代码2

         这里使用了线程池ThreadPool去建立新的线程,免去了自己新建线程要多写的代码。线程池的方法QueueUserWorkItem的第一个参数是一个带object参数的委托,该委托完成线程里要做的事情,ClientProcess方法中就是服务器和客户通信的操作;第二个参数就是委托方法是使用的参数,这里是将建立的socket作为参数传递给ClientProcess。代码3是ClinetProcess的实现,然而ClientProcees中并没有实现具体的通信,而只是执行了一个新的委托ConnectionEstablished,该事件参数SocketEventArgs直接继承自EventArgs,仅包含一个Socket类型的成员,故该委托会将建立的Socket作为事件参数再次传递出去,供事件处理使用。这样做的原因是为了将通信协议的实现和服务器进程的建立脱离开来,这样不论是修改协议实现甚至是以后更换协议都较为方便。另外需要说明的一点是,无论是委托还是事件只是C#对“观察者”模式的一种高级实现,它们和事件通知的代码在同一线程中执行。

[csharp] view plain copy print?
  1. void ClientProcess(object socket)  
  2. {  
  3.      Socket client = (Socket)socket;  
  4.      if (ConnectionEstablished != null)  
  5.      {  
  6.          ConnectionEstablished(this.serverSocket, new SocketEventArgs(client));  
  7.      }  
  8. }  
  9. public event EventHandler<SocketEventArgs> ConnectionEstablished;  
代码3

         另外服务器程序可能还需要同时做其他事,不能在主线程里一直等待新客户连接(Accept方法是阻塞方法),所以等待客户连接也必须由一个专门的线程负责。因此,将上述操作都放入一个类TcpManager中,如代码4所示。

[csharp] view plain copy print?
  1. public class TcpManager  
  2.     {  
  3.         private Socket serverSocket;  
  4.         private string IP;  
  5.         private int port;  
  6.         //...  
  7.         public TcpManager(string ipAddress, int port)  
  8.         {  
  9.             this.IP = ipAddress;  
  10.             this.port = port;  
  11.         }  
  12.         public void StartServer(int backlog)  
  13.         {  
  14.             serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
  15.             IPAddress serverIP = IPAddress.Parse(IP);  
  16.             IPEndPoint serverhost = new IPEndPoint(serverIP, port);  
  17.             serverSocket.Bind(serverhost);  
  18.             serverSocket.Listen(backlog);  
  19.             serverRunning = true;  
  20.             ThreadPool.QueueUserWorkItem((obj) =>  
  21.             {  
  22.                 while (serverRunning)  
  23.                 {  
  24.                     try  
  25.                     {  
  26.                         Socket client = serverSocket.Accept();  
  27.                         ThreadPool.QueueUserWorkItem(ClientProcess, client);  
  28.                         System.Diagnostics.Debug.WriteLine("I'm thread:{0}, I create a new socket", System.Threading.Thread.CurrentThread.ManagedThreadId);  
  29.   
  30.                     }  
  31.                     catch (Exception e)  
  32.                     {  
  33.                         System.Diagnostics.Debug.WriteLine("Exit the server");  
  34.                         break;  
  35.                     }  
  36.                 }  
  37.                 serverSocket.Close();  
  38.             }  
  39.             );  
  40.   
  41.         }  
  42.         private void ClientProcess(object socket)  
  43.         {  
  44.             Socket client = (Socket)socket;  
  45.             try  
  46.             {  
  47.                 if (ConnectionEstablished != null)  
  48.                 {  
  49.                     ConnectionEstablished(this.serverSocket, new SocketEventArgs(client));  
  50.                 }  
  51.             }  
  52.             catch (Exception e)  
  53.             {  
  54.                 System.Diagnostics.Debug.WriteLine(e.Message);  
  55.             }  
  56.             finally  
  57.             {  
  58.                 client.Close();  
  59.             }  
  60.         }  
  61.         public event EventHandler<SocketEventArgs> ConnectionEstablished;  
  62.     }  
  63.     public class SocketEventArgs : EventArgs  
  64.     {  
  65.         public Socket Socket;  
  66.         public SocketEventArgs(Socket socket) { this.Socket = socket; }  
  67.     }  
代码4

         在代码4的ClientProcess中,将整个ConnectionEstablished事件放入了try…catch块中,而捕获的方式仅仅使用了基类Exception,这样做的确有点草率,让人无法判断并处理各种异常,所以具体的异常处理最好是在ConnectionEstablished中完成,我这样按最大捕获的原因主要是考虑最好不要跨线程抛出异常,宁愿直接关闭连接。

3.实现通信协议

         磨叽到现在,终于要开始实现通信协议了,不过这也是为了完成一个结构清晰的设计。参照图1,除了数据传输外,其他部分都是顺序地执行,所以实现起来还是很简单的,如代码5所示。

[csharp] view plain copy print?
  1. public class TcpBasedProcedure  
  2.     {  
  3.         public void SendMessage(Socket socket, MessageEntity me){}  
  4.         public MessageEntity ReceiveMessage(Socket socket){}  
  5.         public void DoProcedureCore(Socket _socket)  
  6.         {  
  7.             byte[] data=new byte[1024];  
  8.             SendMessage(_socket, new MessageEntity(MessageEntity.CommandType.ServerReady, null));  
  9.             MessageEntity hello = ReceiveMessage(_socket);  
  10.             System.Diagnostics.Debug.WriteLine("Received:{0}\n", hello.Command);  
  11.   
  12.             SendMessage(_socket,new MessageEntity(MessageEntity.CommandType.OK,null));  
  13.             MessageEntity r=null;  
  14.             while (true)  
  15.             {  
  16.                 r=ReceiveMessage(_socket);  
  17.                 if(r.Command==MessageEntity.CommandType.Quit)  
  18.                     break;  
  19.                 else if(r.Command==MessageEntity.CommandType.Data){  
  20.                     string str=(string)r.Data;  
  21.                     System.Diagnostics.Debug.WriteLine("Received:{0}\n", str);  
  22.                 }  
  23.             }  
  24.             if(r.Command==MessageEntity.CommandType.Quit)  
  25.                 SendMessage(_socket,new MessageEntity(MessageEntity.CommandType.AgreeDisconnect,null));  
  26.             System.Diagnostics.Debug.WriteLine("Received:{0}\n", r.Command);  
  27.         }  
  28.     }  
代码5

         TcpBasedProcedure类封装了我们要实现的通信协议,协议的主要内容都在DoProcedureCore方法中,当接受了客户端连接后,服务端发送一个命令类型为“ServerReady”,消息数据为空的MessageEntity表示服务器就绪,然后等待客户端回送一个“HelloServer”的消息实体,收到此消息后服务器再发送一条“OK”消息就可以开始通信了,通信过程中如果接收到命令为“Quit”的请求,那么服务器就回送“AgreeDisconnect”命令同意关闭连接,终止本次通信。注意,这里为了代码清晰略去了消息验证的代码,实际上没次接收消息都要验证消息命令。代码中收发的都是string数据,你也可以将Image等二进制对象作为数据发送,需要注意的只有一点,那就是赋值给Data的对象必须可序列化。另外,MessageEntity仅定义了一部分命令,要完成实际的协议可能还需要定义更多的命令,甚至在不更改MessageEntity的情况下利用Data存放一部分自定义命令。

         在代码5中略去了发送和接收实体消息的代码,发送消息如代码6所示。

[csharp] view plain copy print?
  1. public const int MessageSizeWidth=8;  
  2. public void SendMessage(Socket socket, MessageEntity me)  
  3. {  
  4.     IFormatter serializer = new BinaryFormatter();  
  5.     MemoryStream ms = new MemoryStream();  
  6.     serializer.Serialize(ms, me);  
  7.     byte[] data = ms.ToArray();  
  8.     long length = data.Length;  
  9.     if (length == 0)  
  10.         return;  
  11.     socket.Send(BitConverter.GetBytes((long)length));  
  12.     socket.Send(data);  
  13. }  
代码6

         这里可以看到发送时连续发送了两块数据,首8个字节是消息体的大小,其后紧接的才是消息体。消息体是使用二进制序列化到内存流,转化为字节数组后再由Socket发送出去。这样发送主要是为了解决Tcp网络流无边界的问题,这样每次接收都先接受8字节获取消息的长度,然后根据该长度再接收消息体,接收的代码如代码7所示。

[csharp] view plain copy print?
  1. public const int MessageSizeWidth=8;  
  2. public MessageEntity ReceiveMessage(Socket socket)  
  3. {  
  4.     byte[] data = new byte[1024];  
  5.     int readCount = socket.Receive(data, MessageSizeWidth, SocketFlags.None);  
  6.     long length = BitConverter.ToInt64(data, 0);  
  7.     if (length <= 0)  
  8.         return null;  
  9.     int rev = 0;  
  10.     MemoryStream ms = new MemoryStream();  
  11.     int size=(int)length-rev;  
  12.     while ((readCount = socket.Receive(data, size,SocketFlags.None)) != 0)  
  13.     {  
  14.         ms.Write(data, 0, readCount);  
  15.         rev += readCount;  
  16.         if (rev >= length)  
  17.             break;  
  18.         size = (int)length - rev;  
  19.     }  
  20.       
  21.     IFormatter serializer = new BinaryFormatter();  
  22.     ms.Position = 0;  
  23.     MessageEntity me = serializer.Deserialize(ms) as MessageEntity;  
  24.     ms.Close();  
  25.     return me;  
  26.   
  27. }  
代码7

         在接收消息的代码中,使用了while来循环接收,将每次接收到的数据缓存到内存流中,直到接收了一次消息全部的数据后才退出循环,最后将收到的消息反序列化得到消息实体。代码8列出了TcpBasedProcedure的结构,略去了实现代码。

[csharp] view plain copy print?
  1. public class TcpBasedProcedure  
  2. {  
  3.     public const int MessageSizeWidth=8;  
  4.     public void SendMessage(Socket socket, MessageEntity me){}  
  5.     public MessageEntity ReceiveMessage(Socket socket){}  
  6.     public void DoProcedureCore(Socket _socket){}  
  7. }  
代码8

         那么现在就可以启动服务器了,如代码9所示。当我们想要更改协议时,更换一个ConnectionEstablished事件订阅者就可以,而修改协议也仅需要修改TcpBasedProcedure即可。

[csharp] view plain copy print?
  1. class Program  
  2. {  
  3.     static void Main(string[] args)  
  4.     {  
  5.         TcpManager tm = new TcpManager("127.0.0.1", 13000);  
  6.         tm.ConnectionEstablished += Test;  
  7.         tm.StartServer(10);  
  8.         Console.ReadKey();  
  9.     }  
  10.     static void Test(object sender, SocketEventArgs e)  
  11.     {  
  12.         Socket s=e.Socket;  
  13.         TcpBasedProcedure tp = new TcpBasedProcedure();  
  14.         tp.DoProcedureCore(s);  
  15.     }  
  16. }  
代码9

3.实现客户端

         服务器实现了,那么客户端就简单很多了,这里仅是一个简单示例,不多赘述,如代码10所示。

[csharp] view plain copy print?
  1. static void Connect(String serverIP)  
  2.         {  
  3.             try  
  4.             {  
  5.                 Int32 port = 13000;  
  6.                 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
  7.                 IPAddress ip = IPAddress.Parse(serverIP);  
  8.                 IPEndPoint host = new IPEndPoint(ip, port);  
  9.                 socket.Connect(host);  
  10.                 TcpBasedProcedure tbp = new TcpBasedProcedure();  
  11.                 MessageEntity me = tbp.ReceiveMessage(socket);  
  12.                 Console.WriteLine("received:{0} ready", me.Command);  
  13.   
  14.                 me = new MessageEntity(MessageEntity.CommandType.HelloServer, "Hello " + serverIP);  
  15.                 tbp.SendMessage(socket, me);  
  16.   
  17.                 me = tbp.ReceiveMessage(socket);  
  18.                 Console.WriteLine("received:{0} ok", me.Command);  
  19.                 string str = null;  
  20.                 while ((str = Console.ReadLine()) != "close")  
  21.                 {  
  22.                     tbp.SendMessage(socket, new MessageEntity(MessageEntity.CommandType.Data, str));  
  23.                     Console.WriteLine("Sent: {0}", str);  
  24.                 }  
  25.                 tbp.SendMessage(socket, new MessageEntity(MessageEntity.CommandType.Quit,null));  
  26.                 me = tbp.ReceiveMessage(socket);  
  27.                   
  28.                 Console.WriteLine("received:{0} agree close", me.Command);  
  29.                 Console.WriteLine("close connection...\n Press any key continue");  
  30.                 socket.Close();  
  31.                 Console.ReadKey();  
  32.             }  
  33.             catch (SocketException e)  
  34.             {  
  35.                 Console.WriteLine("SocketException: {0}", e);  
  36.             }  
  37.   
  38.         }  
代码10

4.报错?

         实际上这样做下来,是无法完成通信的,要么服务器提示找不到MessageEntity就是客户端找不到。这是因为我们采用了实体数据作为消息载体,尽管服务器和客户端定义了相同的MessageEntity类,但它们可并不是一码事(如果是,那也就天下大乱了)。所以,对于MessageEntity,必须单独建立一个类库工程,将它编译成类库文件若为Me.dll,然后删除服务器和客户端的MessageEntity类,并将Me.dll添加到各自工程的引用中,如图2所示。

Socket编程——基于TCP实现自己的通信协议

图2

此时就能正常通信了。这里也可以看出使用二进制流的弊端,它没有良好的兼容性,所以也可以使用另外两种序列化的方式:XML和SOAP,序列化的方法也类似,这里就不讲了。