Socket编程——基于TCP实现自己的通信协议
2012-08-07 11:08
836 查看
假如我们要做一个C/S型的程序设计,服务端和客户端使用TCP通信,这时就需要在TCP协议之上,选择一个合适的应用层协议,如果不喜欢已有的协议,那就需要自己去实现一个协议规程,现在我们就要去完成一个图1所示的协议。
图1
1.定义传输的消息格式
该协议基本类似于简单邮件传输协议SMTP,不过我们需要做一下改变:信息传输不局限于ASCII码,要能够传输任何对象,这里采用了传输实体类,将其序列化为二进制流进行传输,代码1是定义的消息MessageEntity,注意它使用了Serializable标记,否则无法对它进行序列化,另外,传输的数据也必须被Serializable标记。
代码1
2.实现服务器
现在知道了通信的协议流程和通信消息体,那么就可以开始编程了,首先完成服务器端的程序。对服务器的要求是能允许多客户连接。那么,每次当服务器接受一个连接后,就新开一个线程,由该线程继续去处理和客户端的通信事宜,而服务器回到监听状态,继续等待新的连接。如代码2所示。
这里使用了线程池ThreadPool去建立新的线程,免去了自己新建线程要多写的代码。线程池的方法QueueUserWorkItem的第一个参数是一个带object参数的委托,该委托完成线程里要做的事情,ClientProcess方法中就是服务器和客户通信的操作;第二个参数就是委托方法是使用的参数,这里是将建立的socket作为参数传递给ClientProcess。代码3是ClinetProcess的实现,然而ClientProcees中并没有实现具体的通信,而只是执行了一个新的委托ConnectionEstablished,该事件参数SocketEventArgs直接继承自EventArgs,仅包含一个Socket类型的成员,故该委托会将建立的Socket作为事件参数再次传递出去,供事件处理使用。这样做的原因是为了将通信协议的实现和服务器进程的建立脱离开来,这样不论是修改协议实现甚至是以后更换协议都较为方便。另外需要说明的一点是,无论是委托还是事件只是C#对“观察者”模式的一种高级实现,它们和事件通知的代码在同一线程中执行。
另外服务器程序可能还需要同时做其他事,不能在主线程里一直等待新客户连接(Accept方法是阻塞方法),所以等待客户连接也必须由一个专门的线程负责。因此,将上述操作都放入一个类TcpManager中,如代码4所示。
在代码4的ClientProcess中,将整个ConnectionEstablished事件放入了try…catch块中,而捕获的方式仅仅使用了基类Exception,这样做的确有点草率,让人无法判断并处理各种异常,所以具体的异常处理最好是在ConnectionEstablished中完成,我这样按最大捕获的原因主要是考虑最好不要跨线程抛出异常,宁愿直接关闭连接。
3.实现通信协议
磨叽到现在,终于要开始实现通信协议了,不过这也是为了完成一个结构清晰的设计。参照图1,除了数据传输外,其他部分都是顺序地执行,所以实现起来还是很简单的,如代码5所示。
TcpBasedProcedure类封装了我们要实现的通信协议,协议的主要内容都在DoProcedureCore方法中,当接受了客户端连接后,服务端发送一个命令类型为“ServerReady”,消息数据为空的MessageEntity表示服务器就绪,然后等待客户端回送一个“HelloServer”的消息实体,收到此消息后服务器再发送一条“OK”消息就可以开始通信了,通信过程中如果接收到命令为“Quit”的请求,那么服务器就回送“AgreeDisconnect”命令同意关闭连接,终止本次通信。注意,这里为了代码清晰略去了消息验证的代码,实际上没次接收消息都要验证消息命令。代码中收发的都是string数据,你也可以将Image等二进制对象作为数据发送,需要注意的只有一点,那就是赋值给Data的对象必须可序列化。另外,MessageEntity仅定义了一部分命令,要完成实际的协议可能还需要定义更多的命令,甚至在不更改MessageEntity的情况下利用Data存放一部分自定义命令。
在代码5中略去了发送和接收实体消息的代码,发送消息如代码6所示。
这里可以看到发送时连续发送了两块数据,首8个字节是消息体的大小,其后紧接的才是消息体。消息体是使用二进制序列化到内存流,转化为字节数组后再由Socket发送出去。这样发送主要是为了解决Tcp网络流无边界的问题,这样每次接收都先接受8字节获取消息的长度,然后根据该长度再接收消息体,接收的代码如代码7所示。
在接收消息的代码中,使用了while来循环接收,将每次接收到的数据缓存到内存流中,直到接收了一次消息全部的数据后才退出循环,最后将收到的消息反序列化得到消息实体。代码8列出了TcpBasedProcedure的结构,略去了实现代码。
那么现在就可以启动服务器了,如代码9所示。当我们想要更改协议时,更换一个ConnectionEstablished事件订阅者就可以,而修改协议也仅需要修改TcpBasedProcedure即可。
3.实现客户端
服务器实现了,那么客户端就简单很多了,这里仅是一个简单示例,不多赘述,如代码10所示。
4.报错?
实际上这样做下来,是无法完成通信的,要么服务器提示找不到MessageEntity就是客户端找不到。这是因为我们采用了实体数据作为消息载体,尽管服务器和客户端定义了相同的MessageEntity类,但它们可并不是一码事(如果是,那也就天下大乱了)。所以,对于MessageEntity,必须单独建立一个类库工程,将它编译成类库文件若为Me.dll,然后删除服务器和客户端的MessageEntity类,并将Me.dll添加到各自工程的引用中,如图2所示。
图2
此时就能正常通信了。这里也可以看出使用二进制流的弊端,它没有良好的兼容性,所以也可以使用另外两种序列化的方式:XML和SOAP,序列化的方法也类似,这里就不讲了。
图1
1.定义传输的消息格式
该协议基本类似于简单邮件传输协议SMTP,不过我们需要做一下改变:信息传输不局限于ASCII码,要能够传输任何对象,这里采用了传输实体类,将其序列化为二进制流进行传输,代码1是定义的消息MessageEntity,注意它使用了Serializable标记,否则无法对它进行序列化,另外,传输的数据也必须被Serializable标记。
[Serializable] public class MessageEntity { private CommandType _command; /// <summary> /// 命令类型 /// </summary> public CommandType Command { get { return _command; } set { _command = value; } } private object _data; /// <summary> /// 消息数据 /// </summary> public object Data { get { return _data; } set { _data = value; } } public MessageEntity(CommandType command, object data) { this._command = command; this._data = data; } public enum CommandType { OK, #region Server's Command /// <summary> /// 服务器就绪 /// </summary> ServerReady, /// <summary> /// 允许客户断开 /// </summary> AgreeDisconnect, #endregion #region Client's Command /// <summary> /// 呼叫服务器 /// </summary> HelloServer, /// <summary> /// 数据消息 /// </summary> Data, /// <summary> /// 请求退出 /// </summary> Quit #endregion } }
代码1
2.实现服务器
现在知道了通信的协议流程和通信消息体,那么就可以开始编程了,首先完成服务器端的程序。对服务器的要求是能允许多客户连接。那么,每次当服务器接受一个连接后,就新开一个线程,由该线程继续去处理和客户端的通信事宜,而服务器回到监听状态,继续等待新的连接。如代码2所示。
Socket client = serverSocket.Accept(); ThreadPool.QueueUserWorkItem(ClientProcess, client);代码2
这里使用了线程池ThreadPool去建立新的线程,免去了自己新建线程要多写的代码。线程池的方法QueueUserWorkItem的第一个参数是一个带object参数的委托,该委托完成线程里要做的事情,ClientProcess方法中就是服务器和客户通信的操作;第二个参数就是委托方法是使用的参数,这里是将建立的socket作为参数传递给ClientProcess。代码3是ClinetProcess的实现,然而ClientProcees中并没有实现具体的通信,而只是执行了一个新的委托ConnectionEstablished,该事件参数SocketEventArgs直接继承自EventArgs,仅包含一个Socket类型的成员,故该委托会将建立的Socket作为事件参数再次传递出去,供事件处理使用。这样做的原因是为了将通信协议的实现和服务器进程的建立脱离开来,这样不论是修改协议实现甚至是以后更换协议都较为方便。另外需要说明的一点是,无论是委托还是事件只是C#对“观察者”模式的一种高级实现,它们和事件通知的代码在同一线程中执行。
void ClientProcess(object socket) { Socket client = (Socket)socket; if (ConnectionEstablished != null) { ConnectionEstablished(this.serverSocket, new SocketEventArgs(client)); } } public event EventHandler<SocketEventArgs> ConnectionEstablished;代码3
另外服务器程序可能还需要同时做其他事,不能在主线程里一直等待新客户连接(Accept方法是阻塞方法),所以等待客户连接也必须由一个专门的线程负责。因此,将上述操作都放入一个类TcpManager中,如代码4所示。
public class TcpManager代码4
{
private Socket serverSocket;
private string IP;
private int port;
//...
public TcpManager(string ipAddress, int port)
{
this.IP = ipAddress;
this.port = port;
}
public void StartServer(int backlog)
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress serverIP = IPAddress.Parse(IP);
IPEndPoint serverhost = new IPEndPoint(serverIP, port);
serverSocket.Bind(serverhost);
serverSocket.Listen(backlog);
serverRunning = true;
ThreadPool.QueueUserWorkItem((obj) =>
{
while (serverRunning)
{
try
{
Socket client = serverSocket.Accept(); ThreadPool.QueueUserWorkItem(ClientProcess, client);
System.Diagnostics.Debug.WriteLine("I'm thread:{0}, I create a new socket", System.Threading.Thread.CurrentThread.ManagedThreadId);
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine("Exit the server");
break;
}
}
serverSocket.Close();
}
);
}
private void ClientProcess(object socket)
{
Socket client = (Socket)socket;
try
{
if (ConnectionEstablished != null)
{
ConnectionEstablished(this.serverSocket, new SocketEventArgs(client));
}
}
catch (Exception e)
{
System.Diagnostics.Debug.WriteLine(e.Message);
}
finally
{
client.Close();
}
}
public event EventHandler<SocketEventArgs> ConnectionEstablished;
}
public class SocketEventArgs : EventArgs
{
public Socket Socket;
public SocketEventArgs(Socket socket) { this.Socket = socket; }
}
在代码4的ClientProcess中,将整个ConnectionEstablished事件放入了try…catch块中,而捕获的方式仅仅使用了基类Exception,这样做的确有点草率,让人无法判断并处理各种异常,所以具体的异常处理最好是在ConnectionEstablished中完成,我这样按最大捕获的原因主要是考虑最好不要跨线程抛出异常,宁愿直接关闭连接。
3.实现通信协议
磨叽到现在,终于要开始实现通信协议了,不过这也是为了完成一个结构清晰的设计。参照图1,除了数据传输外,其他部分都是顺序地执行,所以实现起来还是很简单的,如代码5所示。
public class TcpBasedProcedure { public void SendMessage(Socket socket, MessageEntity me){} public MessageEntity ReceiveMessage(Socket socket){} public void DoProcedureCore(Socket _socket) { byte[] data=new byte[1024]; SendMessage(_socket, new MessageEntity(MessageEntity.CommandType.ServerReady, null)); MessageEntity hello = ReceiveMessage(_socket); System.Diagnostics.Debug.WriteLine("Received:{0}\n", hello.Command); SendMessage(_socket,new MessageEntity(MessageEntity.CommandType.OK,null)); MessageEntity r=null; while (true) { r=ReceiveMessage(_socket); if(r.Command==MessageEntity.CommandType.Quit) break; else if(r.Command==MessageEntity.CommandType.Data){ string str=(string)r.Data; System.Diagnostics.Debug.WriteLine("Received:{0}\n", str); } } if(r.Command==MessageEntity.CommandType.Quit) SendMessage(_socket,new MessageEntity(MessageEntity.CommandType.AgreeDisconnect,null)); System.Diagnostics.Debug.WriteLine("Received:{0}\n", r.Command); } }代码5
TcpBasedProcedure类封装了我们要实现的通信协议,协议的主要内容都在DoProcedureCore方法中,当接受了客户端连接后,服务端发送一个命令类型为“ServerReady”,消息数据为空的MessageEntity表示服务器就绪,然后等待客户端回送一个“HelloServer”的消息实体,收到此消息后服务器再发送一条“OK”消息就可以开始通信了,通信过程中如果接收到命令为“Quit”的请求,那么服务器就回送“AgreeDisconnect”命令同意关闭连接,终止本次通信。注意,这里为了代码清晰略去了消息验证的代码,实际上没次接收消息都要验证消息命令。代码中收发的都是string数据,你也可以将Image等二进制对象作为数据发送,需要注意的只有一点,那就是赋值给Data的对象必须可序列化。另外,MessageEntity仅定义了一部分命令,要完成实际的协议可能还需要定义更多的命令,甚至在不更改MessageEntity的情况下利用Data存放一部分自定义命令。
在代码5中略去了发送和接收实体消息的代码,发送消息如代码6所示。
public const int MessageSizeWidth=8; public void SendMessage(Socket socket, MessageEntity me) { IFormatter serializer = new BinaryFormatter(); MemoryStream ms = new MemoryStream(); serializer.Serialize(ms, me); byte[] data = ms.ToArray(); long length = data.Length; if (length == 0) return; socket.Send(BitConverter.GetBytes((long)length)); socket.Send(data); }代码6
这里可以看到发送时连续发送了两块数据,首8个字节是消息体的大小,其后紧接的才是消息体。消息体是使用二进制序列化到内存流,转化为字节数组后再由Socket发送出去。这样发送主要是为了解决Tcp网络流无边界的问题,这样每次接收都先接受8字节获取消息的长度,然后根据该长度再接收消息体,接收的代码如代码7所示。
public const int MessageSizeWidth=8; public MessageEntity ReceiveMessage(Socket socket) { byte[] data = new byte[1024]; int readCount = socket.Receive(data, MessageSizeWidth, SocketFlags.None); long length = BitConverter.ToInt64(data, 0); if (length <= 0) return null; int rev = 0; MemoryStream ms = new MemoryStream(); int size=(int)length-rev; while ((readCount = socket.Receive(data, size,SocketFlags.None)) != 0) { ms.Write(data, 0, readCount); rev += readCount; if (rev >= length) break; size = (int)length - rev; } IFormatter serializer = new BinaryFormatter(); ms.Position = 0; MessageEntity me = serializer.Deserialize(ms) as MessageEntity; ms.Close(); return me; }代码7
在接收消息的代码中,使用了while来循环接收,将每次接收到的数据缓存到内存流中,直到接收了一次消息全部的数据后才退出循环,最后将收到的消息反序列化得到消息实体。代码8列出了TcpBasedProcedure的结构,略去了实现代码。
public class TcpBasedProcedure { public const int MessageSizeWidth=8; public void SendMessage(Socket socket, MessageEntity me){} public MessageEntity ReceiveMessage(Socket socket){} public void DoProcedureCore(Socket _socket){} }代码8
那么现在就可以启动服务器了,如代码9所示。当我们想要更改协议时,更换一个ConnectionEstablished事件订阅者就可以,而修改协议也仅需要修改TcpBasedProcedure即可。
class Program { static void Main(string[] args) { TcpManager tm = new TcpManager("127.0.0.1", 13000); tm.ConnectionEstablished += Test; tm.StartServer(10); Console.ReadKey(); } static void Test(object sender, SocketEventArgs e) { Socket s=e.Socket; TcpBasedProcedure tp = new TcpBasedProcedure(); tp.DoProcedureCore(s); } }代码9
3.实现客户端
服务器实现了,那么客户端就简单很多了,这里仅是一个简单示例,不多赘述,如代码10所示。
static void Connect(String serverIP) { try { Int32 port = 13000; Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ip = IPAddress.Parse(serverIP); IPEndPoint host = new IPEndPoint(ip, port); socket.Connect(host); TcpBasedProcedure tbp = new TcpBasedProcedure(); MessageEntity me = tbp.ReceiveMessage(socket); Console.WriteLine("received:{0} ready", me.Command); me = new MessageEntity(MessageEntity.CommandType.HelloServer, "Hello " + serverIP); tbp.SendMessage(socket, me); me = tbp.ReceiveMessage(socket); Console.WriteLine("received:{0} ok", me.Command); string str = null; while ((str = Console.ReadLine()) != "close") { tbp.SendMessage(socket, new MessageEntity(MessageEntity.CommandType.Data, str)); Console.WriteLine("Sent: {0}", str); } tbp.SendMessage(socket, new MessageEntity(MessageEntity.CommandType.Quit,null)); me = tbp.ReceiveMessage(socket); Console.WriteLine("received:{0} agree close", me.Command); Console.WriteLine("close connection...\n Press any key continue"); socket.Close(); Console.ReadKey(); } catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); } }代码10
4.报错?
实际上这样做下来,是无法完成通信的,要么服务器提示找不到MessageEntity就是客户端找不到。这是因为我们采用了实体数据作为消息载体,尽管服务器和客户端定义了相同的MessageEntity类,但它们可并不是一码事(如果是,那也就天下大乱了)。所以,对于MessageEntity,必须单独建立一个类库工程,将它编译成类库文件若为Me.dll,然后删除服务器和客户端的MessageEntity类,并将Me.dll添加到各自工程的引用中,如图2所示。
图2
此时就能正常通信了。这里也可以看出使用二进制流的弊端,它没有良好的兼容性,所以也可以使用另外两种序列化的方式:XML和SOAP,序列化的方法也类似,这里就不讲了。
相关文章推荐
- socket编程 -- 基于TCP协议的C/S通信模型及实现
- Linux网络编程之[基于socket通信的tcp协议的编程模型]
- Java Socket(四)编程实现基于 TCP 的 Socket 通信
- linux下第一个socket编程实现的局域网内通信(基于TCP)
- Java Socket应用(五)——编程实现基于 TCP 的 Socket 通信
- socket编程 -- 基于UDP协议的C/S通信模型及实现
- Socket编程——基于TCP实现自己的通信协议
- Android Socket编程基于TCP实现客户端与服务器简易通信
- C语言编写基于TCP和UDP协议的Socket通信程序示例
- 基于TCP协议之——socket编程
- 利用socket自己实现基于HTTP协议的Web客户端
- 如何在socket编程的Tcp连接中实现心跳协议
- linux网络编程之用socket实现简单客户端和服务端的通信(基于TCP)
- 使用C#实现基于TCP和UDP协议的网络通信程序的基本示例
- uc笔记10---网络通信,套接字(Socket),基于 TCP 协议的客户机/服务器模型
- 基于TCP的socket通信,实现加减乘除和文件写入(方法二)
- 基于socket简单通信协议实现(c/c++)
- Java的Socket通信----通过 Socket 实现 TCP 编程之多线程demo(2)
- 网络编程_TCP_Socket通信_聊天室_私聊_构思_实现JAVA193-194
- Socket 之 TCP 协议通信-c#实现