.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窗体设计成如下界面:
接下来我们在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){//创建内存缓冲区,大小为1Mbyte[] 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{//客户端套接字连接到网络节点上,用connectsocketClient.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是支持断线自动重连的。这个工作就交给有兴趣的同学了,把代码改造一下。支持不论客户端掉线还是服务端掉线,重启后都能重连。
出处:https://www.tnblog.net/18896101294/article/details/3770
支持小微:
腾讯云 爆款2核2G云服务器首年40元,2G4核云服务器298元/3年
链接:https://curl.qcloud.com/1VVs7OBH
右下角,您点一下在看图片
小微工资涨1毛
商务合作QQ:185601686
