tnblog
首页
视频
资源
登录

C# socket通信的实现与原理

8482人阅读 2020/5/20 15:41 总访问:144531 评论:4 收藏:2 手机
分类: C#

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窗体设计成如下界面:

Winform界面设计


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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System.Text;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. namespace SocketService
  10. {
  11. class Program
  12. {
  13. //和客户端通信的套接字
  14. static Socket client_socket = null;
  15. //集合:存储客户端信息
  16. static Dictionary<string, Socket> clientConnectionItems = new Dictionary<string, Socket> { };
  17. static void Main(string[] args)
  18. {
  19. try
  20. {
  21. //和客户 端通信的套接字:监听客户端发来的消息,三个参数: IP4寻 址协议,流式连接,TCP协议
  22. client_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  23. //服务端发送信息需要一个IP地址和端口号
  24. IPAddress address = IPAddress.Parse("127.0.0.1");
  25. //将IP地址和端口号绑定到网络节点point上
  26. IPEndPoint point = new IPEndPoint(address, 5000);//5000端口用来监听,为本机未占用端口
  27. //监听绑定的网络节点
  28. client_socket.Bind(point);
  29. client_socket.Listen(20);
  30. Console.WriteLine($"开始监听....");
  31. WatchConnecting();
  32. }
  33. catch (Exception ex)
  34. {
  35. Console.WriteLine(ex.Message);
  36. }
  37. }
  38. /// <summary>
  39. /// 业务处理
  40. /// </summary>
  41. private static void WatchConnecting()
  42. {
  43. Socket connection = null;
  44. //持续监听客户端发来的请求
  45. while (true)
  46. {
  47. try
  48. {
  49. connection = client_socket.Accept();
  50. }
  51. catch (Exception ex)
  52. {
  53. //套接字监听异常
  54. Console.WriteLine("套接字监听异常:" + ex.Message);
  55. break;
  56. }
  57. //获取客户端IP、端口
  58. IPAddress clientIp = (connection.RemoteEndPoint as IPEndPoint).Address;
  59. int clientPort = (connection.RemoteEndPoint as IPEndPoint).Port;
  60. //让客户端显示连接成功的信息
  61. string senMsg = "连接服务端成功!\r\n" + "本地IP:" + clientIp + "端口:" + clientPort;
  62. byte[] arrSendMsg = Encoding.UTF8.GetBytes(senMsg);
  63. connection.Send(arrSendMsg);
  64. //客户端网络节点号
  65. string remoteEndPoint = connection.RemoteEndPoint.ToString();
  66. //显示与客户端连接情况
  67. Console.WriteLine("成功与" + remoteEndPoint + "客户端建立连接!\t\n");
  68. //添加客户端信息
  69. clientConnectionItems.Add(remoteEndPoint, connection);
  70. IPEndPoint netpoint = connection.RemoteEndPoint as IPEndPoint;
  71. //创建一个线程通信
  72. ParameterizedThreadStart pts = new ParameterizedThreadStart(revc);
  73. Thread thread = new Thread(pts);
  74. //设置后台进程随主线程退出而退出
  75. thread.IsBackground = true;
  76. thread.Start(connection);
  77. }
  78. }
  79. /// <summary>
  80. /// 接口客户端发来的消息,客户端套接字对象
  81. /// </summary>
  82. private static void revc(object socketclientpara)
  83. {
  84. Socket socketServer = socketclientpara as Socket;
  85. while (true)
  86. {
  87. //创建内存缓冲区,大小为1M
  88. byte[] arrServiceRecMsg = new byte[1024 * 1024];
  89. //将接收到的信息放入到内存缓冲区,并返回其字节数组的长度
  90. try
  91. {
  92. int length = socketServer.Receive(arrServiceRecMsg);
  93. //转换成字符串
  94. string strRecMsg = Encoding.UTF8.GetString(arrServiceRecMsg, 0, length);
  95. Console.WriteLine("客户端:" + socketServer.RemoteEndPoint + "时间:" + DateTime.Now.ToString() +
  96. "\r\n" + strRecMsg + "\r\n\n");
  97. socketServer.Send(Encoding.UTF8.GetBytes("收到了信息"));
  98. }
  99. catch (Exception ex)
  100. {
  101. clientConnectionItems.Remove(socketServer.RemoteEndPoint.ToString());
  102. Console.WriteLine("Client Count:" + clientConnectionItems.Count);
  103. //提示套接字监听异常
  104. Console.WriteLine("客户端" + socketServer.RemoteEndPoint + "已经连接中断\r\n");
  105. Console.WriteLine(ex.Message + "\r\n" + ex.StackTrace + "\r\n");
  106. //关闭之前accept出来的和客户端进行通信的套接字
  107. socketServer.Close();
  108. break;
  109. }
  110. }
  111. }
  112. }
  113. }

客户端代码实现:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Diagnostics;
  6. using System.Drawing;
  7. using System.Linq;
  8. using System.Net;
  9. using System.Net.Sockets;
  10. using System.Runtime.InteropServices;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. using System.Windows.Forms;
  15. namespace WindowsFormsApp1
  16. {
  17. public partial class Form1 : Form
  18. {
  19. //创建一个客户端套接字和一个负责监听服务端请求的线程
  20. Thread threadClient = null;
  21. Socket socketClient = null;
  22. public Form1()
  23. {
  24. InitializeComponent();
  25. StartPosition = FormStartPosition.CenterParent;
  26. //关闭对文本框的非法线程操作检查
  27. TextBox.CheckForIllegalCrossThreadCalls = false;
  28. this.button1.Enabled = false;
  29. this.button1.Visible = false;
  30. this.textBox1.Visible = false;
  31. }
  32. private void Button1_Click(object sender, EventArgs e)
  33. {
  34. //调用 ClientSendMsg 方法,将文本框中输入的信息发送到服务器
  35. ClientSendMsg(this.textBox1.Text.Trim());
  36. this.textBox1.Clear();
  37. }
  38. private void Button2_Click(object sender, EventArgs e)
  39. {
  40. this.button2.Enabled = false;
  41. //定义一个套接字监听
  42. socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  43. IPAddress address = IPAddress.Parse("127.0.0.1");
  44. //将IP、端口绑定到网络节点上
  45. IPEndPoint point = new IPEndPoint(address, 5000);
  46. try
  47. {
  48. //客户端套接字连接到网络节点上,用connect
  49. socketClient.Connect(point);
  50. this.button1.Enabled = true;
  51. this.button1.Visible = true;
  52. this.textBox1.Visible = true;
  53. }
  54. catch (Exception ex)
  55. {
  56. Debug.WriteLine("连接失败\r\n");
  57. this.richTextBox1.AppendText("连接失败\r\n");
  58. this.button2.Enabled = true;
  59. return;
  60. }
  61. threadClient = new Thread(recv);
  62. threadClient.IsBackground = true;
  63. threadClient.Start();
  64. }
  65. /// <summary>
  66. /// 接口客户端发来的消息
  67. /// </summary>
  68. void recv()
  69. {
  70. int x = 0;
  71. //持续监听服务端发来的消息
  72. while (true)
  73. {
  74. try
  75. {
  76. //定义一个1M缓冲区,用于临时存储接受到的消息
  77. byte[] arrRecvmsg = new byte[1024 * 1024];
  78. int length = socketClient.Receive(arrRecvmsg);
  79. string strRevMsg = Encoding.UTF8.GetString(arrRecvmsg, 0, length);
  80. if (x == 1)
  81. {
  82. this.richTextBox1.AppendText($"服务器:{DateTime.Now.ToString()}\r\n{strRevMsg}\r\n\n");
  83. Debug.WriteLine($"服务器:{DateTime.Now.ToString()}\r\n{strRevMsg}\r\n\n");
  84. }
  85. else
  86. {
  87. this.richTextBox1.AppendText(strRevMsg + "\r\n");
  88. Debug.WriteLine($"{strRevMsg}\r\n");
  89. x = 1;
  90. }
  91. }
  92. catch (Exception ex)
  93. {
  94. Debug.WriteLine("远程服务器已经中断连接\r\n");
  95. this.richTextBox1.AppendText("远程服务器已经中断连接\r\n");
  96. }
  97. }
  98. }
  99. /// <summary>
  100. /// 发送字符信息到服务端
  101. /// </summary>
  102. /// <param name="sendMsg"></param>
  103. void ClientSendMsg(string sendMsg)
  104. {
  105. try
  106. {
  107. byte[] arrClientSendMsg = Encoding.UTF8.GetBytes(sendMsg);
  108. //调用客户端套接字发送字节数组
  109. socketClient.Send(arrClientSendMsg);
  110. this.richTextBox1.AppendText($"Hello....:{DateTime.Now.ToString()}\r\n{sendMsg}\r\n\n");
  111. }
  112. catch (Exception ex)
  113. {
  114. Debug.WriteLine("远程服务器已经中断连接\r\n");
  115. this.richTextBox1.AppendText("远程服务器已经中断连接\r\n");
  116. }
  117. }
  118. }
  119. }

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

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

小声bb:话说今天我生日啊!

只要你主动我们就会有故事哦

另外祝大家520节日快乐:

评价

尘叶心繁

2020/5/20 16:42:14

https://github.com/AiDaShi/TimeSynchronization

饰心

2020/5/20 17:15:08

另外附上superSocket的一个讲解比较全面的文章:https://www.cnblogs.com/springsnow/p/9544285.html

饰心

2020/5/21 9:26:32

源码地址:https://download.tnblog.net/resource/index/d6288d4400a3434ba9cca2ba093a7567

忘掉过去式

2021/7/15 17:36:00

大佬流批[太开心]

Decorating heart
排名
22
文章
14
粉丝
21
评论
27
腾讯防水墙
赖成龙 : 学长你有下载好的js文件吗
使用 JSON WEB TOKEN (jwt) 验证
饰心 : 由于最近换了新的工作环境,还在挖煤中。后续会增加博客更新频率。
腾讯防水墙
饰心 : @剑轩,快去给tnblog弄一个
使用select2实现下拉框中显示图片
剑轩 : 秀啊.....,飞常不错
使用select2实现下拉框中显示图片
饰心 : 嗯~刚好差不多下班
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2025TNBLOG.NET
技术交流:群号656732739
联系我们:contact@tnblog.net
公网安备:50010702506256
欢迎加群交流技术
亲近你的朋友,但更要亲近你的敌人。