C#网络编程自学笔记——快速实现客户端服务器通信

这个系列文章将会对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()方法接收服务器消息,注意需要开启新线程防止阻塞。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341