本文通过分析和总结 C#中 Socket 通信编程的关键技术,按照同步实现的方式实现了一个简单的 Socket 聊天程序,目的是通过这个程序来掌握 Socket 编程,为进一步开发 Unity3D 网络游戏打下一个坚实的基础。
Socket 编程基础
关于 Socket 编程基础部分的内容,主要是了解和掌握.NET 框架下为 Socket 编程提供的相关类和接口方法。.NET 中常见的网络相关的 API 都集中在 System.Net 和 System.Net.Socket 这两个命名空间下,大家可以通过 MSDN 去了解这两个命名空间下相关的类和方法。这里援引一位朋友总结的一篇文章http://www.cnblogs.com/sunev/archive/2012/08/05/2604189.html,大家可以从这里获得更为直观的认识。
什么是 Socket 编程的同步实现
本文的目的是按照同步实现的方式来实现一个简单的 Socket 聊天程序,因此在解决这个问题前,我们首先来看看什么是 Socket 编程的同步实现。所谓 Socket 编程的同步实现就是指按照同步过程的方法来实现 Socket 通信。从编程来说,我们常用的方法或者函数都是同步过程。因为当我们调用一个方法或者函数的时候我们能够立即得到它的返回值。可是我们知道在 Socket 通信中,我们不能保证时时刻刻连接都通畅、更不能够保证时时刻刻都有数据收发,因为我们就需要不断去读取相应的值来确定整个过程的状态。这就是 Socket 编程的同步实现了,下面我们来看具体的实现过程。
如何实现 Socket 同步通信
服务端
服务端的主要职责是处理各个客户端发送来的数据,因此在客户端的 Socket 编程中需要使用两个线程来循环处理客户端的请求,一个线程用于监听客户端的连接情况,一个线程用于监听客户端的消息发送,当服务端接收到客户端的消息后需要将消息处理后再分发给各个客户端。
基本流程
- 创建套接字
- 绑定套接字的 IP 和端口号——Bind()
- 将套接字处于监听状态等待客户端的连接请求——Listen()
- 当请求到来后,接受请求并返回本次会话的套接字——Accept()
- 使用返回的套接字和客户端通信——Send()/Receive()
- 返回,再次等待新的连接请求
- 关闭套接字
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
| using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading;
namespace TCPLib { public class TCPServer { private byte[] result = new byte[1024]; private int maxClientCount; public int MaxClientCount { get { return maxClientCount; } set { maxClientCount = value; } }
private string ip; public string IP { get { return ip; } set { ip = value; } }
private int port; public int Port { get { return port; } set { port = value; } }
private List<Socket> mClientSockets; public List<Socket> ClientSockets { get { return mClientSockets; } }
private IPEndPoint ipEndPoint;
private Socket mServerSocket;
private Socket mClientSocket; public Socket ClientSocket { get { return mClientSocket; } set { mClientSocket = value; } }
public TCPServer(int port, int count) { this.ip = IPAddress.Any.ToString(); this.port = port; this.maxClientCount=count;
this.mClientSockets = new List<Socket>();
this.ipEndPoint = new IPEndPoint(IPAddress.Any, port); this.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.mServerSocket.Bind(this.ipEndPoint); this.mServerSocket.Listen(maxClientCount); }
public TCPServer(string ip,int port,int count) { this.ip = ip; this.port = port; this.maxClientCount = count;
this.mClientSockets = new List<Socket>();
this.ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); this.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.mServerSocket.Bind(this.ipEndPoint); this.mServerSocket.Listen(maxClientCount);
}
public void Start() { var mServerThread = new Thread(this.ListenClientConnect); mServerThread.Start(); }
private void ListenClientConnect() { bool flag = true; while (flag) { this.ClientSocket = this.mServerSocket.Accept(); this.mClientSockets.Add(this.ClientSocket); this.SendMessage(string.Format("客户端{0}已成功连接到服务器", this.ClientSocket.RemoteEndPoint)); var mReveiveThread = new Thread(this.ReceiveClient); mReveiveThread.Start(this.ClientSocket); } }
private void ReceiveClient(object obj) { var mClientSocket = (Socket)obj; bool flag = true; while (flag) { try { int receiveLength = mClientSocket.Receive(result); string clientMessage = Encoding.UTF8.GetString(result, 0, receiveLength); this.SendMessage(string.Format("客户端{0}发来消息:{1}",mClientSocket.RemoteEndPoint,clientMessage));
} catch (Exception e) { this.mClientSockets.Remove(mClientSocket); this.SendMessage(string.Format("服务器发来消息:客户端{0}从服务器断开,断开原因:{1}",mClientSocket.RemoteEndPoint,e.Message)); mClientSocket.Shutdown(SocketShutdown.Both); mClientSocket.Close(); break; } } }
public void SendMessage(string msg) { if (msg == string.Empty || this.mClientSockets.Count <= 0) return; foreach (Socket s in this.mClientSockets) { (s as Socket).Send(Encoding.UTF8.GetBytes(msg)); } }
public void SendMessage(string ip,int port,string msg) { IPEndPoint _IPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port); foreach (Socket s in mClientSockets) { if (_IPEndPoint == (IPEndPoint)s.RemoteEndPoint) { s.Send(Encoding.UTF8.GetBytes(msg)); } } } } }
|
好了,现在我们已经编写好了一个具备接收和发送数据能力的服务端程序。现在我们来尝试让服务端运行起来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| using System; using System.Collections.Generic; using System.Text; using TCPLib; using System.Net; using System.Net.Sockets;
namespace TCPLib.Test { class Program { static void Main(string[] args) { TCPLib.TCPServer s1 = new TCPServer("127.0.0.1", 6001, 10); TCPLib.TCPServer s2 = new TCPServer(6001, 10); s1.Start(); } }
}
|
现在我们来看看编写客户端 Socket 程序的基本流程
客户端
客户端相对于服务端来说任务要轻许多,因为客户端仅仅需要和服务端通信即可,可是因为在和服务器通信的过程中,需要时刻保持连接通畅,因此同样需要两个线程来分别处理连接情况的监听和消息发送的监听。
基本流程
- 创建套接字保证与服务器的端口一致
- 向服务器发出连接请求——Connect()
- 和服务器端进行通信——Send()/Receive()
- 关闭套接字
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
| using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading;
namespace TCPLib { public class TCPClient { private byte[] result = new byte[1024];
private string ip; public string IP { get { return ip; } set { ip = value; } }
private int port; public int Port { get { return port; } set { port = value; } }
private IPEndPoint ipEndPoint;
private Socket mClientSocket;
private bool isConnected = false;
public TCPClient(string ip, int port) { this.ip=ip; this.port=port; this.ipEndPoint = new IPEndPoint(IPAddress.Parse(this.ip), this.port); mClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
public void Start() { var mConnectThread = new Thread(this.ConnectToServer); mConnectThread.Start(); }
private void ConnectToServer() { while (!isConnected) { try { mClientSocket.Connect(this.ipEndPoint); this.isConnected = true; } catch (Exception e) { Console.WriteLine(string.Format("因为一个错误的发生,暂时无法连接到服务器,错误信息为:{0}",e.Message)); this.isConnected = false; }
Thread.Sleep(5000); Console.WriteLine("正在尝试重新连接..."); }
Console.WriteLine("连接服务器成功,现在可以和服务器进行会话了"); var mReceiveThread = new Thread(this.ReceiveMessage); mReceiveThread.Start(); }
private void ReceiveMessage() { bool flag = true; while (flag) { try { int receiveLength = this.mClientSocket.Receive(result); string serverMessage = Encoding.UTF8.GetString(result, 0, receiveLength); Console.WriteLine(serverMessage); } catch (Exception e) { flag = false; this.mClientSocket.Shutdown(SocketShutdown.Both); this.mClientSocket.Close();
this.isConnected = false; ConnectToServer(); } } }
public void SendMessage(string msg) { if(msg==string.Empty || this.mClientSocket==null) return;
mClientSocket.Send(Encoding.UTF8.GetBytes(msg)); } } }
|
同样地,我们现在来运行客户端程序,这样客户端就可以和服务端进行通信了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| using System; using System.Collections.Generic; using System.Text; using TCPLib; using System.Net; using System.Net.Sockets;
namespace TCPLib.Test { class Program { static void Main(string[] args) { TCPLib.TCPClient c = new TCPClient("127.0.0.1",6001); c.Start(); while(true) { string msg = Console.ReadLine(); c.SendMessage(msg); } } }
}
|
注意要先运行服务端的程序、再运行客户端的程序,不然程序会报错,嘿嘿!好了,下面是今天的效果演示图:
聊天窗口效果演示
客户端下线效果演示
总结
今天我们基本上写出了一个可以使用的用例,不过这个例子目前还存在以下问题:
这里仅仅实现了发送字符串的功能,如何让这个程序支持更多的类型,从基础的 int、float、double、string、single 等类型到 structure、class 甚至是二进制文件的类型?
如何让这个用例更具有扩展性,我们发现所有的 Socket 编程流程都是一样的,唯一不同就是在接收到数据以后该如何去处理,因为能不能将核心功能和自定义功能分离开来?
在今天的这个用例中,数据传输的缓冲区大小我们人为设定为 1024,那么如果碰到比这个设定更大的数据类型,这个用例该怎么来写?
好了,这就是今天的内容了,希望大家喜欢,同时希望大家关注我的博客!
2016 年 1 月 24 日更新:
要解决“支持更多类型的问题”,可以从两种思路来考虑,即实现所有类型到 byte[]类型的转换或者是实现所有类型到 string 类型的转换,对于第二种思路我们通常称之为序列化,序列化可以解决所有类型到 string 类型的转换问题,唯一可能需要考量的一个部分就是缓冲区的大小问题。
要解决“将核心功能和自定义功能分离”这个问题,可以考虑使用委托机制来实现,委托机制可以理解为一个函数的指针,在需要将函数的控制权交给用户来处理的场景中,委托都是一种有效而明智的选择。