vlambda博客
学习文章列表

.NET中 socket通信的实现与原理

本篇文章是本人通过自己的理解进行整理的,如有疑问欢迎指出
在说socket之前我们先大致了解一下进程之间通信的几种方式(了解下就好了):

  • 管道

管道分为匿名管道和命名管道

类型 描述
匿名管道 用一根竖线表示,没有名字
命名管道 可以通过mkfifo test创建管道,其中test为管道名称

我们来看一条 Linux 的语句: netstat -tulnp | grep 8080
其中“|”是管道的意思,它的作用就是把前一条命令的输出作为后一条命令的输入。在这里就是把 netstat -tulnp 的输出结果作为 grep 8080 这条命令的输入。
在前端angular框架中其实也有类似管道的应用,比如下面的代码:
<span *ngIf="col.Name==='operDt'" [innerHTML]="rowNode[col.Name] | date:'yyyy-MM-dd'"></span>
这段代码的含义就是把operDt的输出值按照'yyyy-MM-dd'格式显示。

管道的通知机制类似于缓存,就像一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是管道是单向传输的。
这种通信方式有什么缺点呢?显然,这种通信方式效率低下,你看,a 进程给 b 进程传输数据,只能等待 b 进程取了数据之后 a 进程才能返回。
所以管道不适合频繁通信的进程。当然,他也有它的优点,例如比较简单,能够保证我们的数据已经真的被其他进程拿走了。我们平时用 Linux 的时候,也算是经常用。

这种通信方式有缺点吗?答是有的,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。
哪有没有什么解决方案呢?答是有的,请继续往下看。


  • 消息队列

那我们能不能把进程的数据放在某个内存之后就马上让进程返回呢?无需等待其他进程来取就返回呢?
答是可以的,我们可以用消息队列的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的
消息队列里取出来。同理,b 进程要个 a 进程发送消息也是一样。这种通信方式也类似于缓存吧。

这种通信方式有缺点吗?答是有的,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。


  • 共享内存


  • 信号量

共享内存最大的问题是什么?没错,就是多进程竞争内存的问题,就像类似于我们平时说的线程安全问题。如何解决这个问题?这个时候我们的信号量就上场了。
信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。


  • Socket

上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?
答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

就目前而言,几乎所有的应用程序都是采用socket


了解了进程间通信的方式,我们现在就来写一个socket通信的例子:

为了快速演示,这里我们就创建一个Winform程序充当客户端,一个控制台应用程序充当服务端端,项目结构如下:


其中客户端Winform窗体设计成如下界面:

.NET中 socket通信的实现与原理


接下来我们在Program.cs中实现服务端代码:

using System;using System.Collections.Generic;using System.Linq;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading;using System.Threading.Tasks;namespace SocketService{ class Program { //和客户端通信的套接字 static Socket client_socket = null; //集合:存储客户端信息 static Dictionary<string, Socket> clientConnectionItems = new Dictionary<string, Socket> { }; static void Main(string[] args) { try { //和客户 端通信的套接字:监听客户端发来的消息,三个参数: IP4寻 址协议,流式连接,TCP协议 client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //服务端发送信息需要一个IP地址和端口号 IPAddress address = IPAddress.Parse("127.0.0.1"); //将IP地址和端口号绑定到网络节点point上 IPEndPoint point = new IPEndPoint(address, 5000);//5000端口用来监听,为本机未占用端口 //监听绑定的网络节点 client_socket.Bind(point); client_socket.Listen(20); Console.WriteLine($"开始监听...."); WatchConnecting(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } /// <summary> /// 业务处理 /// </summary> private static void WatchConnecting() { Socket connection = null; //持续监听客户端发来的请求 while (true) { try { connection = client_socket.Accept(); } catch (Exception ex) { //套接字监听异常 Console.WriteLine("套接字监听异常:" + ex.Message); break; } //获取客户端IP、端口 IPAddress clientIp = (connection.RemoteEndPoint as IPEndPoint).Address; int clientPort = (connection.RemoteEndPoint as IPEndPoint).Port; //让客户端显示连接成功的信息 string senMsg = "连接服务端成功!\r\n" + "本地IP:" + clientIp + "端口:" + clientPort; byte[] arrSendMsg = Encoding.UTF8.GetBytes(senMsg); connection.Send(arrSendMsg); //客户端网络节点号 string remoteEndPoint = connection.RemoteEndPoint.ToString(); //显示与客户端连接情况 Console.WriteLine("成功与" + remoteEndPoint + "客户端建立连接!\t\n"); //添加客户端信息 clientConnectionItems.Add(remoteEndPoint, connection); IPEndPoint netpoint = connection.RemoteEndPoint as IPEndPoint; //创建一个线程通信 ParameterizedThreadStart pts = new ParameterizedThreadStart(revc); Thread thread = new Thread(pts); //设置后台进程随主线程退出而退出 thread.IsBackground = true; thread.Start(connection); } } /// <summary> /// 接口客户端发来的消息,客户端套接字对象 /// </summary> private static void revc(object socketclientpara) { Socket socketServer = socketclientpara as Socket; while (true) { //创建内存缓冲区,大小为1M byte[] arrServiceRecMsg = new byte[1024 * 1024]; //将接收到的信息放入到内存缓冲区,并返回其字节数组的长度 try { int length = socketServer.Receive(arrServiceRecMsg); //转换成字符串 string strRecMsg = Encoding.UTF8.GetString(arrServiceRecMsg, 0, length); Console.WriteLine("客户端:" + socketServer.RemoteEndPoint + "时间:" + DateTime.Now.ToString() + "\r\n" + strRecMsg + "\r\n\n"); socketServer.Send(Encoding.UTF8.GetBytes("收到了信息")); } catch (Exception ex) { clientConnectionItems.Remove(socketServer.RemoteEndPoint.ToString()); Console.WriteLine("Client Count:" + clientConnectionItems.Count); //提示套接字监听异常 Console.WriteLine("客户端" + socketServer.RemoteEndPoint + "已经连接中断\r\n"); Console.WriteLine(ex.Message + "\r\n" + ex.StackTrace + "\r\n"); //关闭之前accept出来的和客户端进行通信的套接字 socketServer.Close(); break; } } } }}

客户端代码实现:

using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Diagnostics;using System.Drawing;using System.Linq;using System.Net;using System.Net.Sockets;using System.Runtime.InteropServices;using System.Text;using System.Threading;using System.Threading.Tasks;using System.Windows.Forms;namespace WindowsFormsApp1{ public partial class Form1 : Form { //创建一个客户端套接字和一个负责监听服务端请求的线程 Thread threadClient = null; Socket socketClient = null; public Form1() { InitializeComponent(); StartPosition = FormStartPosition.CenterParent; //关闭对文本框的非法线程操作检查 TextBox.CheckForIllegalCrossThreadCalls = false; this.button1.Enabled = false; this.button1.Visible = false; this.textBox1.Visible = false; } private void Button1_Click(object sender, EventArgs e) { //调用 ClientSendMsg 方法,将文本框中输入的信息发送到服务器 ClientSendMsg(this.textBox1.Text.Trim()); this.textBox1.Clear(); } private void Button2_Click(object sender, EventArgs e) { this.button2.Enabled = false; //定义一个套接字监听 socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress address = IPAddress.Parse("127.0.0.1"); //将IP、端口绑定到网络节点上 IPEndPoint point = new IPEndPoint(address, 5000); try { //客户端套接字连接到网络节点上,用connect socketClient.Connect(point); this.button1.Enabled = true; this.button1.Visible = true; this.textBox1.Visible = true; } catch (Exception ex) { Debug.WriteLine("连接失败\r\n"); this.richTextBox1.AppendText("连接失败\r\n"); this.button2.Enabled = true; return; } threadClient = new Thread(recv); threadClient.IsBackground = true; threadClient.Start(); } /// <summary> /// 接口客户端发来的消息 /// </summary> void recv() { int x = 0; //持续监听服务端发来的消息 while (true) { try { //定义一个1M缓冲区,用于临时存储接受到的消息 byte[] arrRecvmsg = new byte[1024 * 1024]; int length = socketClient.Receive(arrRecvmsg); string strRevMsg = Encoding.UTF8.GetString(arrRecvmsg, 0, length); if (x == 1) { this.richTextBox1.AppendText($"服务器:{DateTime.Now.ToString()}\r\n{strRevMsg}\r\n\n"); Debug.WriteLine($"服务器:{DateTime.Now.ToString()}\r\n{strRevMsg}\r\n\n"); } else { this.richTextBox1.AppendText(strRevMsg + "\r\n"); Debug.WriteLine($"{strRevMsg}\r\n"); x = 1; } } catch (Exception ex) { Debug.WriteLine("远程服务器已经中断连接\r\n"); this.richTextBox1.AppendText("远程服务器已经中断连接\r\n"); } } } /// <summary> /// 发送字符信息到服务端 /// </summary> /// <param name="sendMsg"></param> void ClientSendMsg(string sendMsg) { try { byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(sendMsg); //调用客户端套接字发送字节数组 socketClient.Send(arrClientSendMsg); this.richTextBox1.AppendText($"Hello....:{DateTime.Now.ToString()}\r\n{sendMsg}\r\n\n"); } catch (Exception ex) { Debug.WriteLine("远程服务器已经中断连接\r\n"); this.richTextBox1.AppendText("远程服务器已经中断连接\r\n"); } } }}


代码不算难,也还算好理解,分步调试一下就能懂了,测试一下,分别启动服务端、客户端项目,运行演示,本来想搞个gif动图演示的,markdown上传gif失败了。懒得转成文字博客了:

.NET中 socket通信的实现与原理

socket是支持断线自动重连的。这个工作就交给有兴趣的同学了,把代码改造一下。支持不论客户端掉线还是服务端掉线,重启后都能重连。

出处:https://www.tnblog.net/18896101294/article/details/3770



支持小微:

腾讯云 爆款2核2G云服务器首年40,2G4核云服务器298元/3年

链接:https://curl.qcloud.com/1VVs7OBH


右下角,您点一下在看图片

小微工资涨1毛

商务合作QQ:185601686