C#笔记7 网络通信抽象,Socket类的介绍和简单使用
一、背景介绍
在前面不算详细的基础知识和基本编程背景下,我们开始了今天重头菜,也就是开始与远方的计算机建立起一个连接,正式打通计算机与计算机之间的桥梁。
C#笔记6 网络编程基础,解释端口套接字,代码实例分析DNS,IPAddress等类-CSDN博客
前文我们讲了计算机中间运行着怎么样的连接,介绍了C#中使用什么类和方法获取IP和主机名,并且获取一系列特殊的ip地址。
今天之后要学习的是网络通信的内容,后续使用的几个类:socket,TcpClient,TcpListener,UDPClient。
二、基础概念了解
首先这是一种涉及到网络内容的编程,不同设备上底层读取网络或许有所不同,实际的代码在设备上运行对应的底层实现也不会完全相同,不去了解底层,只有浅薄的知识,那就只能在这个方面浅尝辄止。
服务端与客户端
这就是我们要讲的概念了,如果你是初学者你也许到目前为止所有的程序都在同一个文件中编写,乃至于使用多文件但是在同一个程序中运行,但是在我们这一类型的编程中 我们其实是在两台计算机上运行的程序中间传递数据。也就是我们编写运行的程序分两个部分:服务端程序和客户端程序。
多线程
这也是网络编程中常见的概念,尤其是对于服务端而言,可能需要主动使用多线程来处理客户端的连接。我们在本次编程中需要使用新的线程处理连接或者监听端口,而不能让此类行为阻塞我们整个服务端程序。
socket和tcp,ip以及HTTP等的关系
Socket 是一种位于 TCP 层之上的抽象,它通过提供简单的 API,让应用层开发者能够使用 TCP 协议来进行可靠的网络通信。所以你可以理解为,Socket 在上层,对应的是 TCP(或 UDP),它负责将应用层与传输层连接起来。
很多人以为tcplistener中包含对socket类的封装,就认为是不是TCP的层次要高于socket,事实上并不是,Tcp只是socket的一个传输方法,socket的意义是建立一个抽象的管道提供给应用层用于传递数据,底层是udp还是tcp其实不是一定的,并且不止这两种协议,事实上还有其他协议通过这一抽象接口提供给上层应用。后面我们构造socket的时候就会看到了。
所以TCPlistener虽然包含对socket类的封装,但是它本质上不代表tcp协议,只是代表这一类使用socket接口对应的底层协议是tcp,在我看来,它的地位和socket是不同的,是一种被限制的socket的使用方式的封装。
socket为什么说是一种位于传输层上的抽象,因为他的本质就是提供一个接口给应用传递数据,甚至于可以使用tcp,udp,unix域套接字,甚至是直接接入ip这一级别,构建自己的传输层协议。也就是说,你甚至可以自定义一种包的格式来作为传输格式,设立一系列的报文规范。
HTTP是应用层的协议,我们在建立起socket连接之后发送的数据包的内容中可能就包含某种应用层协议的请求。
地址族
前文中已经提到过这一概念, 也就是在IPAddress类的字段中存在这一个概念,其为一个枚举变量,对应几十种地址的分类,上次我们检测到我们的ip地址族为internetwork,有的老师会解释成内网,但是实际上,这意思就是指ipv4地址。
三、socket通信
Socket 类 (System.Net.Sockets) | Microsoft Learn
首先介绍Socket类,关于这一类的讨论很多,有人讨论到底能建立多少个socket连接啦,到底怎么监听新建连接之类的话题。
我们首先来学习怎么建立一个TCP协议的socket连接。
准备socket连接
构造函数:
我们在代码中首先要使用指定的地址族、套接字类型和协议初始化 Socket 类的新实例。
Socket(AddressFamily, SocketType, ProtocolType)
Socket(SocketType, ProtocolType)
可以看到第二个构造函数给出了两个参数的构造方法,不给出地址族的socket通信默认是构造IPv4和IPv6两个协议族的双栈套接字。
使用指定的地址族、套接字类型和协议初始化 Socket 类的新实例。 如果操作系统支持 IPv6,则此构造函数将创建双模式套接字;否则,它将创建 IPv4 套接字。
IPEndPoint意为网络终端点,我们既然要建立连接,自然是有发送方和接收方,或者说服务端和客户端,即使将两边看做相等地位的pc也需要知道两个端点,之前我们说了,ip是一个电脑在网络上的地址,这里的IPEndPoint是EndPoint的子类,我们之前也说了,socket不一定就是和ip打交道,乃至于本地的进程通信和蓝牙通信都可能用到socket。这之后需要的就不是IPEndPoint了,而是会用到EndPoint抽象类的其他子类。
实质上,IPEndPoint就是ip地址和一个端口的组合。
我们介绍它的目的是引出socket建立连接的第二步,就是绑定端口和 ip到我们的socket的一端。如果你是服务端,这意味着你只要选好自己要用来接待的房子和用来进房子的门(对应ip地址和电脑的端口,ip地址就是你房子的地址,端口就是你房子的窗口,服务端需要选一个窗口在那里等待属于你这个程序的链接和数据。)
无论是作为服务器还是客户端,都需要知道两个端点的信息才能进行通信,那么其中一端我们知道是本机,另外一端呢?事实上可以是很多种设备,甚至是设备本地,如何做到,也许你忘记了我们网络地址中特殊的地址:环回地址。
环回地址
127.0.0.1事实上任何发送到该端口的数据包都会被立刻转发到本地,也就是如果你发送数据,目标选择这一地址,最后收到数据的会是本机。
// 创建服务端的 IPEndPoint(监听在本地 127.0.0.1 的 8080 端口)
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8080);
// 创建服务器 Socket
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(serverEndPoint); // 绑定到终端点
serverSocket.Listen(10); // 开始监听连接请求
Console.WriteLine("服务器正在监听...");
我们这里的操作,可以说就是把一端设为本地这么简单,但是,这和设置客户端是不同的,服务端设置了本地监听为环回地址,而这个地址只有本地能够访问,你应该意识到问题了。
也就是说,这里的服务端程序只会和自己本地跑的客户端程序能够建立连接。常用于测试程序。
如果在实际生产环境中一般使用全0地址,或者说监听所有ip地址,也有针对对方访问的ip来限制连接的,比如指定内外网访问的资源不同,使用内网ip访问和外网就不一样。毕竟一个设备可能有很多ip,使用什么ip监听连接,就决定了应用程序在什么地址的房子里等待连接,哪怕这些IP地址(房子的地址)都是你设备的地址(可以理解成一个电脑有多个ip地址是因为作为一个房子有多个地址可以填写,比如前后门两个街道都可以抵达,但是这个房子前后门不是一家公司或者说一个用处,比如我们后门只接待内网用户vip,外网就无法访问了,对方必须先访问我们的内网才能到达我们后门)。
代码:获取本机主机名和ip地址实例化IPEndPoint
var hostName = Dns.GetHostName();
IPHostEntry localhost = await Dns.GetHostEntryAsync(hostName);
// This is the IP address of the local machine
IPAddress localIpAddress = localhost.AddressList[0];
IPEndPoint ipEndPoint = new(localIpAddress, 5151);
这里就用到我们前文的知识了。
开始监听
// Socket.Listen函数的摘要:
// Places a System.Net.Sockets.Socket in a listening state.
//
// 参数:
// backlog:
// The maximum length of the pending connections queue.
//
关于服务端的监听,这里使用Listen来创建了一个最多建立十个TCP连接的监听序列。
// 创建服务端的 IPEndPoint(监听在本地 127.0.0.1 的 8080 端口)
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Loopback, 8080);
// 创建服务器 Socket
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(serverEndPoint); // 绑定到终端点
serverSocket.Listen(10); // 开始监听连接请求
到这里已经服务端完成了三个操作:
创建了一个socket(插座),插座一端插在预设的端点,开启一个长度为10的监听序列。
现在轮到我们的客户端了。
发起请求
客户端也需要新建一个socket。接着调用connect方法,去连接一个网络终点
Socket clsoc = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clsoc.Bind(new IPEndPoint(IPAddress.Loopback,5150));
clsoc.Connect(new IPEndPoint(IPAddress.Loopback, 5151));
while(true)
{
byte[] buffer = new byte[1024];
int bytesReceived = clsoc.Receive(buffer);
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesReceived));
}
连接之后进入一个死循环,我们这里没有使用异步方法,只是简单的使用了一个连接请求,如果没有请求成功线程就会阻塞在这里。
服务端也是类似,请看下面代码:
using System.Net.Sockets;
using System.Net;
using System.Text;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(IPAddress.Any, 5151));
socket.Listen(10);
//暂停程序直到有人连接
while (true)
{
Socket clientsocket = socket.Accept();
Console.WriteLine("连接成功");
byte[] msg = Encoding.UTF8.GetBytes("connected!");
clientsocket.Send(msg);
Console.WriteLine("可以继续发送,按回车发送");
//string ch=Console.ReadKey().ToString();
string order = Console.ReadLine();
byte[] msg2 = Encoding.UTF8.GetBytes(order);
clientsocket.Send(msg2);
}
我们也是在创建了队列之后开始循环,可以这么理解,每一个客户端的请求到达之后会在我们Listen设定的队列中等待,如果超过10个就会被我们拒绝或者说抛弃。
在等待队列中的请求会被我们的while循环中的accept方法所处理(接收),到这就是连接建立成功。
此后可以开始发送数据或者接收数据,值得注意的是,我们调用的发送方法是发送一个字节数组,也就是二进制数据,我们需要使用Encoding的方法转化我们的文本为二进制数据,同样接收时也要重新转化回来。
我们到这就知道怎么建立一个socket连接了,但是你会想,这样循环处理请求,如果有多个连接建立,意味着会有更复杂的情况出现,比如:
服务端怎么根据请求的信息对每个请求执行不同的代码?
怎么使用连接发送文件这一类的数据呢?
怎么知道客户端是不是断开连接了?
怎么群发消息给所有客户端?
事实上如果只是实现这些会比你想的简单,但是有一些令人头疼的问题等着你,如果你和我一样是个初学者或者多线程和同步异步的门外汉的话。