这个系列文章将会对C#的网络编程常用方法和编程技巧进行记录。本文主要记录基于TCP/IP协议的本地客户端与华为云电脑上运行的服务端进行通信的编程过程。
C#常用网络通信类
IPEndPoint类
EndPoint可以理解为终端,IPEndPoint理解为以IP方式解释这个终端
这个类是EndPoint抽象类的实现类。
Socket对象有两个类型,一个是RemoteEndPoint类,另一个是LocalEndPoint类,即一个是远程终端,另一个是本地终端。
属性Address:使用IPv4表示的地址;
属性Port:使用int表示的端口号(0-65535),一般数值选择10000以上会比较好,可以避免其他应用占用冲突的问题。
Socket类
Socket类既可以用在服务端的开发,也可以用在客户端的开发,构造时需要三个参数:
1.参数AddressFamily:指定使用的IPv4地址InterNetwork;
2.参数SocketType:指定使用流式传输Stream;
3.参数ProtocolType:指定协议类型Tcp
类方法:
1.Bind()方法:绑定IP与端口,这样就成为了服务器,可以监听指定IP的特定端口;
2.Listen()方法:置为监听状态,参数是允许的最大挂起数。
3.Accept()方法:接收客户端的连接,返回一个Socket对象,此方法会阻塞当前的线程,即如果程序主线程执行到Accept()方法的时候,主线程会停止并且等待客户端的连接而导致后面的代码无法执行,因此在使用的过程中一般需要配合多线程操作执行这个方法,结合多线程的尾递归
来允许接收多个客户端的连接。
4.Receive()方法:接收客户端发送过来的消息,以字节为单位进行操作,此方法同样会阻塞当前的线程,故同样需要开启新的线程来执行这个方法。
5.Send()方法:发送消息,以字节为单位。
在本地计算机创建客户端
首先需要创建一个Socket对象,通过如下代码:
public class ClientControl
{
private Socket clientSocket;
public ClientControl() // 方法名称与类名相同,即为构造函数
{
clientControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
}
接下来我们需要创建一个连接服务端的方法,这样我们就可以调用这个连接方法并且传入ip地址的字符串和整数型的端口号就可以了。
public void Connect(string ip, int port)
{
clientSocket.Connect(ip, port);
}
在云端计算机创建服务端
首先需要创建一个Socket对象,通过如下代码:
public class ServerControl
{
private Socket serverSocket;
public ServerControl() // 方法名称与类名相同,即为构造函数
{
serverControl = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
}
接下来我们需要启动服务器,一般会把下面的代码添加到一个新的方法中,然后在服务端的主函数中调用这个方法来启动服务器的相关服务。
serverSocket.Bind(new IPEndPoint(IPAddress.Parse("xxx.xx.xx.xxx"), xxxxx/*端口号*/));
这里通过parse方法将一个字符串IP地址转换成IPAddress所需要的十六进制表示的IP地址,但是这样做的弊端是如果这个程序转移到其他机器上运行,还需要注意修改这里的IP地址的设置值,因此我们还可以尝试下面的方法:
serverSocket.Bind(new IPEndPoint(IPAddress.Any, xxxxx/*端口号*/));
接下来我们需要服务器对我们设置的端口进行监听
serverSocket.Listen(10); // 开启监听,最大挂起数设置为10
以上内容设置完成之后,我们就需要使能客户端的连接,接收客户端的连接,并且返回这个客户端的Socket对象
Socket client = serverSocket.Accept(); // 注意,这个Accept方法会挂起当前线程
如果我们想获得连接的客户端的ip地址、分配的端口等属性信息,我们可以通过返回的Socket对象来查看,但是Socket对象无法直接获得ip和端口等属性,我们需要调用Socket对象中的
RemoteEndPoint
方法,因为从服务器的角度来说,客户端属于远端的终端。另外一个值得注意的地方就是Socket.RemoteEndPoint是一个EndPoint类型的抽象类,仍然无法直接获得ip地址和端口等属性信息,因此我们需要将这个抽象类用IPEndPoint的类型来解释:
IPEndPoint point = client.RemoteEndPoint as IPEndPoint;
此时,我们就可以获得point对象的Address属性和Port端口属性
Console.WriteLine(point.Address); // 控制台输出客户端ip地址
Console.WriteLine(point.Port); // 控制台输出客户端端口
通过上面的在客户端和服务器端分别创建工程代码,我们就成功实现了一个最简单的服务端与客户端的连接,现在服务端启动调试,然后再启动客户端的调试,如果没有报错,那么就实现了最简单基于TCP/IP通信协议的通信过程。
存在的问题
通过上面的代码创建的客户端和服务器,由于我们在编程的时候没有采用多线程和循环结构,因此服务端最多只能连接一个客户端,并且双方都不具备发送消息的功能,因此下面我们还需要进行完善。
多线程——解决服务端连接多个客户端的问题
之前说到过,Accept()方法是会阻塞当前的线程的,因此我们需要将这个方法放入新的线程之中。
第一步、将服务端接收客户端的代码封装成一个方法
private void Accept()
{
Socket client = serverSocket.Accept(); // 注意,这个Accept方法会挂起当前线程
Accept(); // 尾递归循环执行代码
}
第二步、在服务端的开启服务方法中启动多线程,当然了,如果你把所有绑定端口监听端口等操作都放在了构造函数中的话,那么你可以在构造函数中启动多线程
Thread threadAccept = new Thread(Accept); // 将前面我们写的Accept方法添加到多线程进程之中
threadAccept.IsBackground = true; // 设置此线程是否是背景线程:如果设置为true表示主线程结束的时候此背景线程会立即结束,
//如果设置为false则即使主线程结束,此线程也不会结束。
threadAccept.Start(); // 启动线程
这样当我们启动主机后,在启动多个客户端,服务端都可以响应。
客户端向服务端发送消息
通过send方法发送消息,此方法接收一个字节数组byte[]类型作为参数,因此我们需要把字符串类型的数据转换成字节数组类型。
public void Send(string msg)
{
clientSocket.Send(Encoding.UTF8.GetBytes(msg)); // 将字符串转换成字节数组
}
服务器接收客户端的消息
类似地,在服务端我们通过receive方法来接收来自客户端的消息,这个方法接收一个字节数组作为参数,同时返回一个int类型的值表示接收到的字节个数,接收到的内容将会存放在传入的字节数组参数之中。
byte[] msg = new byte[1024]; // 创建一个字节数组
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen); // 将接收到的字符数组转换为字符串,第二个参数表示转换起始为止
// 第三个参数表示转换结束位置
值得注意的是Receive方法也会阻塞当前的线程,只要程序执行到这个指令,就会一直等待客户端发来消息,如果客户端不发送就会一直等待,后续代码无法继续执行,因此我们仍然需要将这个方法放入新的线程之中,和前面开启新线程的方法类似,区别在于我们需要向这个方法之中传入client对象,因为新线程的数量随着客户端接入的数量增加,不同的线程应该处理不同的客户端,对于需要传递参数的多线程的开启,需要注意多线程中传递的参数只能是object对象,在线程实现方法中,我们需要对object对象进行解释,例子如下:
private void Receive(object obj) // 线程实现方法
{
Socket client = obj as Socket; // 将obj参数解释为Socket对象
byte[] msg = new byte[1024]; // 创建一个字节数组
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen); // 将接收到的字符数组转换为字符串,第二个参数表示转换起始为止
// 第三个参数表示转换结束位置
Receive(client) // 尾递归
}
多线程的启动如下:
Thread threadAccept = new Thread(Receive); // 将前面我们写的Accept方法添加到多线程进程之中
threadAccept.IsBackground = true; // 设置此线程是否是背景线程:如果设置为true表示主线程结束的时候此背景线程会立即结束,
//如果设置为false则即使主线程结束,此线程也不会结束。
threadAccept.Start(client); // 启动线程,并且传递参数client
解决问题——客户端断开连接
上面的代码可以实现客户端向服务端发送消息,服务端接收消息,但是如果遇到客户端主动关闭程序下线,服务端就会抛出异常,问题发生的地方就在于服务端的Receive方法处,因为client对象已经断开,receive方法就会出现错误,因此在这个地方我们需要进行
try
catch
的异常处理环节。
try
{
byte[] msg = new byte[1024]; // 创建一个字节数组
int msgLen = client.Receive(msg);
strMsg = Encoding.UTF8.GetString(msg, 0, msgLen);
}
catch(Exception ex)
{
//…………下线处理………………
}
服务端向客户端发送消息
与客户端向服务端发送消息一样,通过client.Send()方法向客户端发送消息。
客户端接收服务端发送消息
与前面一样,通过clientSocket.Receive()方法接收服务器消息,注意需要开启新线程防止阻塞。