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