C# socket通信的实现与原理 电脑版发表于:2020/5/20 15:41 #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窗体设计成如下界面:  -------- 接下来我们在`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失败了。懒得转成文字博客了:  socket是支持断线自动重连的。这个工作就交给有兴趣的同学了,把代码改造一下。支持不论客户端掉线还是服务端掉线,重启后都能重连。 小声bb:话说今天我生日啊! 只要你主动我们就会有故事哦  另外祝大家520节日快乐: 