你也可以写个聊天程序 C# Socket学习

简述

我们做软件工作的虽然每天都离不开网络,可网络协议细节却不是每个人都会接触和深入了解。我今天就来和大家一起学习下Socket,并写一个简单的聊天程序。

一些基础类

首先我们每天打开浏览器访问网页信息都是使用的HTTP/HTTPS协议,而HTTP是通过的TCP建立的连接。TCP底层又是通过的Socket套接字进行的通信。所以他们之间的抽象关系是:

我们在学习Socket编程的时候可能会需要用到IPEndPoint、Dns、IPAddress等类,再往上TCP相关有TcpListener、TcpClient、NetworkStream,再往上就是大家熟悉的HttpClient等。

IPEndPoint、Dns、IPAddress基础作用如下:

代码语言:javascript
复制
//根据DNS获取域名绑定的IP
foreach (var address in Dns.GetHostEntry("www.baidu.com").AddressList)
{
    Console.WriteLine($"百度IP:{address}");
}
//字符串转IP地址
IPAddress ipAddress = IPAddress.Parse("192.168.1.101");

//通过IP和端口构造IPEndPoint对象,用于远程连接
//通过IP可以确定一台电脑,通过端口可以确定电脑上的一个程序
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 80);

利用Socket编写聊天程序

我们首先从Socket开始讲起。

要实现Socket通信,先得有个服务端的监听,再有个客户端的连接,然后客户端和服务端就可以通信了。如下:

服务端代码如下

代码语言:javascript
复制
//1 创建Socket对象
socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

//2 绑定ip和端口
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001);
socketServer.Bind(ipEndPoint);

//3、开启侦听(等待客户机发出的连接),并设置最大客户端连接数为10
socketServer.Listen(10);

//4、【阻塞】,等待客户端连接
Socket newSocket = socketServer.Accept();

//5、【阻塞】,等待读取客户端发送过来的数据
byte[] data = new byte[1024 * 1024];
int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None);

//6、读取数据
var msg = Encoding.UTF8.GetString(data, 0, readLeng);

客户端代码如下

代码语言:javascript
复制
//1 创建Socket对象
socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

//2 连接到服务端
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001);
socketClient.Connect(ipEndPoint);

//3 发送消息到服务端
socketClient.Send(Encoding.UTF8.GetBytes("你好,农码一生"));

到此,我们就可以开启服务端的服务,并接受客户端的发来的消息了。

不过,这里有个很大的问题,服务端只能建立一个客户端连接和接受一次客户端发来的消息。如果想要连接更多的客户端和接受无数次的消息,服务端代码两处阻塞的地方需要另外开一个线程然后包到循环里面去。

修改后的服务端代码如下:

代码语言:javascript
复制
void .... ()
{
//1 创建Socket对象
socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

//2 绑定ip和端口
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPoint = new IPEndPoint(ip, 50001);
socketServer.Bind(ipEndPoint);

//3、开启侦听(等待客户机发出的连接),并设置最大客户端连接数为10
socketServer.Listen(10);

//开启新的线程,循环等待新的客户端连接
Task.Run(() => { Accept(socketServer); });

}

void Accept(Socket socket)
{
while (true)
{
//4、【阻塞】,等待客户端连接
Socket newSocket = socket.Accept();
//开启新的线程,循环等待接收新的数据
Task.Run(() => { Receive(newSocket); });
}
}

void Receive(Socket newSocket)
{
while (true)
{
//5、【阻塞】,等待读取客户端发送过来的数据
byte[] data = new byte[1024 * 1024];
int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None);
//6、读取数据
var msg = Encoding.UTF8.GetString(data, 0, readLeng);
}
}

到此,服务端就可以接受多个客户端的连接和接收多次客户端发来的消息了。不过我们可能还需客服端能接收服务端发来的消息,这个你可以自己尝试下。文末会提供完整的代码参考。

注意:用Socket来编写聊天软件是长连接,有状态的。不确定服务端什么时候会发送消息过来,我们也可以连续发送消息而不响应。所以,对于消息的接收就需要开一个新的线程循环接收。

而对于HTTP来说,虽然它是也是通过TCP建立的通信,但在数据请求完毕后会马上关闭连接,这个过程很短。每次访问都会建立一个新的连接,是无状态的。

对于浏览器来说是一问一答的形式,先发送请求(Send),然后接收响应(Receive)所以就可以做到不开启新的线程,直接有序的同步的完成。这个在下一篇《模拟浏览器的请求和服务端的响应》会具体分析。

利用TCP编写聊天程序

虽然上面我们利用Socket类实现了一个简单的聊天程序,但是微软觉得Socket太复杂。为了让你们早点干完活,早点下班,于是又在Socket的基础上有封装了两个相关的类TcpListener、TcpClient。

利用TcpListener、TcpClient来实现同上面相同的功能。

服务端代码

代码语言:javascript
复制
void ...()
{
//1 开启监听
TcpListener tcpListener = new TcpListener(IPAddress.Parse("127.0.0.1"), 9999);
tcpListener.Start(10); //最多同时接收10个用户连接

//开启一个线程,循环等待客户端的连接
Task.Run(() => { Accept(); });

}

//等待客户端的连接
void Accept()
{
while (true)
{
//2 【阻塞】等待客户端的连接
TcpClient tcpClient = tcpListener.AcceptTcpClient();
NetworkStream networkStream = tcpClient.GetStream();
//开启一个新的线程 等待新的消息
Task.Run(() => { Read(networkStream, tcpClient); });
}
}

//接收消息
void Read(NetworkStream networkStream)
{
while (true)
{
byte[] buffer = new byte[1024 * 1024];
//3 【阻塞】等待接收新的消息
var readLen = networkStream.Read(buffer, 0, buffer.Length);
var msg = Encoding.UTF8.GetString(buffer, 0, readLen);
}
}

客户端代码

代码语言:javascript
复制
//1 连接服务端
TcpClient tcpClient = new TcpClient();
tcpClient.Connect(IPAddress.Parse(textBox1.Text), int.Parse(textBox2.Text));

//2 发送消息到服务端
byte[] buffer = Encoding.UTF8.GetBytes("你好,农码一生");
networkStream.Write(buffer, 0, buffer.Length);

用TcpListener、TcpClient的实现也算ok了,TcpListener代码写的服务端和Socket通信也是完成没问题的,因为他们最后都是Socket。

对此你有觉得比Socket简单和容易理解?其实我更习惯Socket。

注意:

代码语言:javascript
复制
// 1、断开连接使用
socketClient?.Shutdown(SocketShutdown.Both);
socketClient?.Close();

// 2、服务端需要判断
int readLeng = newSocket.Receive(data, 0, data.Length, SocketFlags.None);
if (readLeng == 0)//客户端断开连接
{
//停止会话(禁用Socket上的发送和接收,该方法允许Socket对象一直等待,直到将内部缓冲区的数据发送完为止)
newSocket.Shutdown(SocketShutdown.Both);
//关闭连接
newSocket.Close();
//跳出循环
return;
}

具体请参考文末提供的完整demo

效果图

结束

  • Demo:https://github.com/zhaopeiym/BlogDemoCode/tree/master/Socket编程

  • 关于demo本来打算用.NET Core 3.0来写的,可是winform的设计器不争气。 所以demo还是用的.net fx,只有TcpListener是用的.NET Core控制台写的。
  • 为什么要写这个,一是学习下底层的协议通信,二是为了实现一套物联网设备通讯协议(https://github.com/zhaopeiym/IoTClient)做准备。