一、什么是Socket
socket编程是网络常用的编程,我们通过在网络中创建socket关键字来实现网络间的进程通信。
1、网络间的进程如何通讯
首先要了解进程间的通讯方式:(win32 API)
参考博客:windows下进程间通信的,
Microsoft Win32应用编程接口(Application Programming Interface, API)提供了大量支持应用程序间数据共享和交换的机制,这些机制行使的活动称为进程间通信(InterProcess Communication, IPC),进程通信就是指不同进程间进行数据共享和数据交换。
- 文件映射(Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。)
- 共享内存(Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。)
- 匿名管道(管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。匿名管道(Anonymous Pipe)是 在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。)
- 命名管道(命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。)
- 邮件槽(邮件槽(Mailslots)提 供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。)
- 剪贴板(剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。)
- 动态数据交换(动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。)
- 对象连接和嵌入(应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据格式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服务。)
- 动态链接库(Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。)
- 远程过程调用(Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。)
- NetBios函数(Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,其它应用程序最好不要使用NetBios函数来进行进程间通信。)
- Sockets(Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。)
- WM_COPYDATA消息(WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。
WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。)
两个进程不在同一台机器上他们的通讯就需要网络,一般都是通过tcp/ip进行网络通讯,他们就是送信的线路和驿站的作用
TCP/IP协议不同于iso的7个分层,它是根据这7个分层
TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
传输层:TCP,UDP
网络层:IP,ICMP,OSPF,EIGRP,IGMP
数据链路层:SLIP,CSLIP,PPP,MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的
有了网络之后就要唯一标识进程,就像写信都会有收件人和发件人,在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。
其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,
而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。
这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,
网络中的进程通信就可以利用这个标志与其它进程进行交互。
2、socket的定义
这样的话如果程序员需要编写一个有网络间进程通讯的软件,他不仅需要了解进程间通讯还需要了解网络间各种协议,
实现起来需要层层包装一条消息,是不是很麻烦,socket就应运而生了:Windows中的很多东西都是从Unix领域借鉴过来的,在Unix中,socket代表了一种文件描述符(在Unix中一切都是以文件为单位),而这里这个描述符则是用于描述网络访问的。
程序员可以通过socket来发送和接收网络上的数据。你也可以理解成是一个API。有了它,你就不用直接去操作网卡了,而是通过这个接口,这样就省了很多复杂的操作。
Socket所处的位置大概是下面这样的。
我们可以发现socket就在应用程序的传输层和应用层之间,设计了一个socket抽象层,传输层的底一层的服务提供给socket抽象层,socket抽象层再提供给应用层
有了这些基本条件,我们就可以用它来访问网络了,具体操作如下:
- 确定本机的IP和端口
- 确定通讯协议(比如TCP 或者 UDP)
- 建立一个套接字
- 绑定本机的IP和端口
- 如果是TCP,因为是面向连接的,所以要利用Listen()方法来监听网络上是否有人给自己发东西;如果是UDP,因为是无连接的,所以来者不拒。
- TCP情况下,如果监听到一个连接,就可以使用accept来接收这个连接,然后就可以利用Send/Receive来执行操作了。而UDP,则不需要accept, 直接使用SendTo/ReceiveFrom来执行操作
- 如果不想继续发送和接收了,就不要浪费资源了。能close的就close吧
下面将一一列出Socket常使用的对象(类和方法)
二、Socket编程常用对象和用法
1 命名空间
using System.Net; using System.Net.Socket;
2 构造新的socket对象
public socket (AddressFamily addressFamily,SocketType sockettype,ProtocolType protocolType)
(1) AddressFamily 用来指定socket解析地址的寻址方案,Inte.Network标示需要ip版本4的地址,Inte.NetworkV6需要ip版本6的地址;
(2) SocketType 参数指定socket类型,Raw支持基础传输协议访问,Stream支持可靠,双向,基于连接的数据流;
- Dgram(支持数据报,即最大长度固定(通常很小)的无连接、不可靠消息。 消息可能会丢失或重复并可能在到达时不按顺序排列。)
- Raw(支持对基础传输协议的访问。)
- Rdm(支持无连接、面向消息、以可靠方式发送的消息,并保留数据中的消息边界。)
- Seqpacket(在网络上提供排序字节流的面向连接且可靠的双向传输。)
- Stream(支持可靠、双向、基于连接的字节流,而不重复数据,也不保留边界。)
(3) ProtocolType 表示socket支持的网络协议,如常用的TCP和UDP协议。
- Tcp传输控制协议。
- Udp用户数据报协议。
- Raw原始 IP 数据包协议。
- IP网际协议。
- PupPARC 通用数据包协议。
- IpxInternet 数据包交换协议。
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
3定义主机对象
IPEndPoint类
public IPEndPoint(IPAddress address,int port)
参数address可以直接填写主机的IP,如"192.168.2.1";
//IPAddress(Byte[]) 新实例初始化 IPAddress 类地址指定为 Byte 数组。; IPAddress ipAddress1 = new IPAddress(new byte[] { 151, 33, 86, 50 }); //IPAddress(Int64) 新实例初始化 IPAddress 类地址指定为 Int64。 IPAddress ipAddress2 = new IPAddress(0x2414188f); //Parse(String) IP 地址将字符串转换为 IPAddress 实例 IPAddress ipAddress3 = IPAddress.Parse("192.168.100.9"); //IPEndPoint 表示主机地址和端口信息。 IPEndPoint ipEndPoint = new IPEndPoint(ipAddress1, 8899); IPAddress[] ips = Dns.GetHostEntry("www.baidu.com").AddressList; ips = Dns.GetHostAddresses("www.cctv.com");
或者
byte[] byts = IPAddress.Parse("192.168.2.1").GetAddressBytes(); Array.Reverse(byts); // 需要倒置一次字节序 long ipadress = BitConverter.ToUInt32(byts, 0); IPEndPoint ipEndPoint2 = new IPEndPoint(ipadress, 8899);
利用DNS服务器解析主机,使用Dns.Resolve方法
public static IPHostEntry Resolve(string hostname)
参数:待解析的主机名称,返回IPHostEntry类值,IPHostEntry为Inte.Net主机地址信息提供容器,该容器提供存有IP地址列表,主机名称等。
Dns.GetHostByName获取本地主机名称
public static IPHostEntry GetHostByName(string hostname)
GetHostByAddress
public static IPHostEntry GetHostByAddress(IPAddress address)
public static IPHostEntry GetHostByAddress(string address)
IPAddress:包含了一个IP地址[提供 Internet 协议 (IP) 地址]
//这里的IP是long类型的,这里使用Parse()可以将string类型数据转成IPAddress所需要的数据类型 IPAddress IP = IPAddress.Parse();
Encoding.ASCII:编码转换
Encoding.ASCII.GetBytes() //将字符串转成字节 Encoding.ASCII.GetString() //将字节转成字符串
4 端口绑定和监听
Connect()
建立远程主机的连接
public void Connect(EndPoint remoteEP);
参数:IPEndPoint对象
Bind()
绑定一个本地的IP和端口号,参数是一个绑定了IP和端口号的IPEndPoint对象
public void Bind(EndPoint localEP)
参数为主机对象 IPEndPoint
Listen()
让Socket侦听传入的连接,参数为指定侦听队列的容量
public void Listen(int backlog)
参数为整型,表示监听挂起队列的最大值
accept()
接收连接并返回一个新的Socket,Accept会中断程序,直到有客户端连接
public socket accept()
返回值是socket对象
Close()
public void Close()
关闭Socket,销毁连接
5 socket的发送和接收方法
发送数据:
public int Send(byte[] buffer)
参数为待发送数据的字节数组
public int Send(byte[],SocketFlags)
SocketFlags成员列表:
DontRoute不使用路由表发送,
MaxIOVectorLength为发送和接收数据的wsabuf结构数量提供标准值,
None 不对次调用使用标志,
OutOfBand消息的部分发送或接收,
Partial消息的部分发送或接收,
Peek查看传入的消息。
public int Send(byte[],int,SocketFlags)
参数多了字节数组的长度
public int Send(byte[],int,int,SocketFlags)
int的参数变成了开始发送的位置和字节数组的长度
NetWordStream类的Write方法
public override void write(byte[] buffer,int offset,int size)
参数:要发送的数组,开始发送位置,发送数组长度
NetWordStream类请看这篇文章:NetWordStream类
接收数据:
Socket类Receive方法
public int Receive(byte[] buffer)
public int Receive(byte[],SocketFlags) public int Receive(byte[],int,SocketFlags) public int Receive(byte[],int,int,SocketFlags)
NetworkStream类的Read方法
public override int Read(int byte[] buffer,int offset,int size)
参数:要读取的数组,开始读取位置,读取的数组长度
三、Socket套接字的TCP和UDP
TCP和UDP是常见的通信协议,我们需要指定套接字的一些参数,例如 IP 地址、端口号、协议等等,以确保通信能够顺利进行。
Socket 连接是一种重要的通信机制。它允许两个程序在不同计算机上进行实时通信,通过套接字的创建和使用来实现数据的传输。
打个比方说:我和小明通电话,我们一定要确认打通了才开始交流,打不通也会有专门的人告你怎么怎么不通;
电话打着打着,欸有一句没听清,可以让对面再说一次,这个是TCP的通信方式,Socket呢,Socket是电话机
我给小王寄信,反正信我是寄出去了,对面收没收到那我就不管了,要是一段时间内对面没回信,那就当对面没收到吧
这种通讯方式就是UDP,Socket呢,Socket这时候就是邮局
所以总结:socket是一种应用程序接口(API),用于在应用程序中访问TCP和UDP协议。通过socket,应用程序可以创建连接、传输数据,实现网络通信功能。
下面是对于UDP的概念:
UDP 是User Datagram Protocol的简称, 中文名是用户数据包协议,是 OSI 参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
它是IETF RFC 768是UDP的正式规范。 UDP报头 UDP报头由4个域组成,其中每个域各占用2个字节,具体如下: 源端口号 目标端口号 数据报长度 校验值 UDP协议使用端口号为不同的应用保留其各自的数据传输通道。UDP和TCP协议正是采用这一机制实现对同一时刻内多项应用同时发送和接收数据的支持。
数据发送一方(可以是客户端或服务器端)将UDP数据报通过源端口发送出去,而数据接收一方则通过目标端口接收数据。
有的网络应用只能使用预先为其预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。
因为UDP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。一般来说,大于49151的端口号都代表动态端口。 数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以该域主要被用来计算可变长度的数据部分(又称为数据负载)。
数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节。
不过,一些实际应用往往会限制数据报的大小,有时会降低到8192字节。 UDP协议使用报头中的校验值来保证数据的安全。校验值首先在数据发送方通过特殊的算法计算得出,在传递到接收方之后,还需要再重新计算。
如果某个数据报在传输过程中被第三方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算值将不会相符,由此UDP协议可以检测是否出错。
这与TCP协议是不同的,后者要求必须具有校验值。 许多链路层协议都提供错误检查,包括流行的以太网协议,也许想知道为什么UDP也要提供检查和。
其原因是链路层以下的协议在源端和终端之间的某些通道可能不提供错误检测。
虽然UDP提供有错误检测,但检测到错误时,UDP不做错误校正,只是简单地把损坏的消息段扔掉,或者给应用程序提供警告信息。 UDP协议的几个特性 (1) UDP是一个无连接协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;
在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。 (2) 由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等,因此一台服务机可同时向多个客户机传输相同的消息。 (3) UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。 (4) 吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制。 (5)UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态表(这里面有许多参数)。 (6)UDP是面向报文的。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。
既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。 虽然UDP是一个不可靠的协议,但它是分发信息的一个理想协议。例如,在屏幕上报告股票市场、在屏幕上显示航空信息等等。 UDP也用在路由信息协议RIP(Routing Information Protocol)中修改路由表。 在这些应用场合下,如果有一个消息丢失,在几秒之后另一个新的消息就会替换它。 UDP广泛用在多媒体应用中,例如,Progressive Networks公司开发的RealAudio软件,它是在因特网上把预先录制的或者现场音乐实时传送给客户机的一种软件, 该软件使用的RealAudio audio-on-demand protocol协议就是运行在UDP之上的协议,大多数因特网电话软件产品也都运行在UDP之上。
下面是TCP的概念
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 [1]定义。 TCP旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠TCP提供可靠的通信服务。TCP假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。 当应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,TCP则把数据流分割成适当长度的报文段,最大传输段大小(MSS)通常受该计算机连接的网络的数据链路层的最大传送单元(MTU)限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。 TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。 TCP是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式: (1)基于流的方式; (2)面向连接; (3)可靠通信方式; (4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销; (5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。 为满足TCP协议的这些特点,TCP协议做了如下的规定: ①数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组; ②到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认; ③超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片; ④滑动窗口:TCP连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出; ⑤失序处理:作为IP数据报来传输的TCP分片到达时可能会失序,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层; ⑥重复处理:作为IP数据报来传输的TCP分片会发生重复,TCP的接收端必须丢弃重复的数据; ⑦数据校验:TCP将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。 TCP的首部格式如图1所示: ---Source Port是源端口,16位。 ---Destination Port是目的端口,16位。 ---Sequence Number是发送数据包中的第一个字节的序列号,32位。 ---Acknowledgment Number是确认序列号,32位。 ---Data Offset是数据偏移,4位,该字段的值是TCP首部(包括选项)长度除以4。 ---标志位: 6位,URG表示Urgent Pointer字段有意义: ACK表示Acknowledgment Number字段有意义 PSH表示Push功能,RST表示复位TCP连接 SYN表示SYN报文(在建立TCP连接的时候使用) FIN表示没有数据需要发送了(在关闭TCP连接的时候使用) Window表示接收缓冲区的空闲空间,16位,用来告诉TCP连接对端自己能够接收的最大数据长度。 ---Checksum是校验和,16位。 ---Urgent Pointers是紧急指针,16位,只有URG标志位被设置时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。
由于.NET框架通过UdpClient、TcpListener 、TcpClient这几个类对Socket进行了封装,使其使用更加方便
我们可以直接使用UdpClient、TcpListener 、TcpClient进行开发
1、UDP基本应用
与TCP通信不同,UDP通信是不分服务端和客户端的,通信双方是对等的。为了描述方便,我们把通信双方称为发送方和接收方。
下面是一个基于UDP的即时聊天小程序
/// <summary> /// 显示数据使用异步的委托 /// </summary> private delegate void InvokeDelegate(); public MainForm() { InitializeComponent(); } //全局变量 Thread t = null; //接收进程、接收显示进程 string RecvData = null; //接收的数据 static Socket UdpClient = null; /// <summary> /// 窗体加载方法 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MainForm_Load(object sender, EventArgs e) { //方法1: //CheckForIllegalCrossThreadCalls = false; this.tB_LocalIp.Text = GetLocalIp(); this.tB_RemoteIp.Text = this.tB_LocalIp.Text; UdpClient = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); IPEndPoint localIPEndPoint = new IPEndPoint(IPAddress.Parse(this.tB_LocalIp.Text), Convert.ToInt16(this.tB_LocalPort.Text)); UdpClient.Bind(localIPEndPoint); //开启线程 t = new Thread(ReciveMsg); t.Start(); } /// <summary> /// 获取本机IP地址 /// </summary> public string GetLocalIp() { ///获取本地的IP地址 string AddressIP = string.Empty; foreach (IPAddress _IPAddress in Dns.GetHostEntry(Dns.GetHostName()).AddressList) { if (_IPAddress.AddressFamily.ToString() == "InterNetwork") { AddressIP = _IPAddress.ToString(); } } return AddressIP; } /// <summary> /// 接收发送给本机ip对应端口号的数据报 /// </summary> void ReciveMsg() { while (true) { EndPoint point = new IPEndPoint(IPAddress.Any, 0); //用来保存发送方的ip和端口号 byte[] buffer = new byte[1024]; int length = UdpClient.ReceiveFrom(buffer, ref point);//接收数据报 //led = Encoding.Default.GetString(buffer, 0, length); //led_ctrl(); RecvData += "【from " + point + "】:" + Encoding.Default.GetString(buffer, 0, length);//Encoding.UTF8.GetString方法不能支持中文 RecvData += "\r\n";//接收完换行 this.Invoke(new InvokeDelegate(DisplayReciveMsg));//方法2: } } /// <summary> /// 显示数据,并滑到最底 /// </summary> public void DisplayReciveMsg() { this.rtB_RecvMsg.Text = RecvData; //让富文本框自动滑到最底行 //让文本框获取焦点 this.rtB_RecvMsg.Focus(); //设置光标的位置到文本尾 this.rtB_RecvMsg.Select(this.rtB_RecvMsg.TextLength-1, 0); //滚动到控件光标处 this.rtB_RecvMsg.ScrollToCaret(); } private void btn_SendMsg_Click(object sender, EventArgs e) { /* 实列化Socket套接字对象 * 参数: * AddressFamily(地址族) :InterNetwork ——> IP 版本 4 的地址 * SocketType(套接字类型):Dgram ——> 数据报 * ProtocolType(支持类型):UDP ——> UDP协议 * */ Socket sSndMag = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); //获取UI中对方的IP地址 IPAddress ipaddrRemote = IPAddress.Parse(this.tB_RemoteIp.Text); //IPEndPoint——>指定地址和端口号 EndPoint edpRemote = new IPEndPoint(ipaddrRemote, Convert.ToInt16(this.tB_RemotePort.Text)); //发送,发送前需要把发送的内容转成字节类型的 sSndMag.SendTo(System.Text.Encoding.Default.GetBytes(this.rtB_SendMsg.Text), edpRemote); //关闭套接字 sSndMag.Close(); } private void btn_ClrSendMsg_Click(object sender, EventArgs e) { rtB_SendMsg.Text = ""; } private void btn_ClrRecvMsg_Click(object sender, EventArgs e) { RecvData = ""; rtB_RecvMsg.Text = ""; }
下载链接:聊天小程序
总结如下:
tcp必须建立连接才可以进行通信
udp不需要建立通信
但是两者都需要一个监听来接收消息
四、Socket编程实现
1.UDPClient
构造函数:
UdpClient() |
初始化 UdpClient 类的新实例。 |
UdpClient(AddressFamily) |
初始化 UdpClient 类的新实例。 |
UdpClient(Int32) |
初始化 UdpClient 类的新实例,并将它绑定到所提供的本地端口号。 |
UdpClient(Int32, AddressFamily) |
初始化 UdpClient 类的新实例,并将它绑定到所提供的本地端口号。 |
UdpClient(IPEndPoint) |
初始化 UdpClient 类的新实例,并将其绑定到指定的本地终结点。 |
UdpClient(String, Int32) |
初始化 UdpClient 类的新实例,并建立默认远程主机。 |
方法
连接方法
Connect(IPAddress, Int32) |
使用指定的 IP 地址和端口号建立默认远程主机。 |
Connect(IPEndPoint) |
使用指定的网络终结点建立默认远程主机。 |
Connect(String, Int32) |
使用指定的主机名和端口号建立默认远程主机。 |
发送方法:
Send(Byte[], Int32) |
将 UDP 数据报发送到远程主机。 |
Send(Byte[], Int32, IPEndPoint) |
将 UDP 数据报发送到位于指定远程终结点的主机。 |
Send(Byte[], Int32, String, Int32) |
将 UDP 数据报发送到指定远程主机上的指定端口。 |
Send(ReadOnlySpan<Byte>) |
将 UDP 数据报发送到远程主机。 |
Send(ReadOnlySpan<Byte>, IPEndPoint) |
将 UDP 数据报发送到位于指定远程终结点的主机。 |
Send(ReadOnlySpan<Byte>, String, Int32) |
将 UDP 数据报发送到指定远程主机上的指定端口。 |
接收消息方法
Receive(IPEndPoint) |
返回由远程主机发送的 UDP 数据报。 |
ReceiveAsync() |
异步返回由远程主机发送的 UDP 数据报。 |
ReceiveAsync(CancellationToken) |
异步返回由远程主机发送的 UDP 数据报。 |
关闭方法
Close() |
关闭 UDP 连接。 |
释放资源
Dispose() |
释放由 UdpClient 占用的托管和非托管资源。 |
Dispose(Boolean) |
释放由 UdpClient 占用的非托管资源,还可以另外再释放托管资源。 |
属性
Active |
获取或设置一个值,该值指示是否已建立默认远程主机。 |
Available |
获取从网络接收的可读取的数据量。 |
Client |
获取或设置基础网络 Socket。 |
DontFragment |
获取或设置 Boolean 值,该值指定 UdpClient 是否允许将 Internet 协议 (IP) 数据报分段。 |
EnableBroadcast | |
ExclusiveAddressUse | |
MulticastLoopback |
获取或设置 Boolean 值,该值指定是否将输出多播数据包传递给发送应用程序。 |
Ttl |
获取或设置一个值,它指定由 UdpClient 发送的 Internet 协议 (IP) 数据包的生存时间 (TTL)。 |
//1.我们先使用Socket的方法来实现 //服务器代码 using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; namespace UDP { class Program { static void Main(string[] args) { int recv; byte[] data = new byte[1024]; //得到本机IP,设置TCP端口号 IPEndPoint ip = new IPEndPoint(IPAddress.Any , 8001); Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram , ProtocolType.Udp); //绑定网络地址 newsock.Bind(ip); Console.WriteLine("This is a Server, host name is {0}",Dns.GetHostName()); //等待客户机连接 Console.WriteLine("Waiting for a client"); //得到客户机IP IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0); EndPoint Remote = (EndPoint)(sender); recv = newsock.ReceiveFrom(data, ref Remote); Console .WriteLine ("Message received from {0}: ", Remote.ToString ()); Console .WriteLine (Encoding .ASCII .GetString (data ,0,recv )); //客户机连接成功后,发送信息 string welcome = "你好 ! "; //字符串与字节数组相互转换 data = Encoding .ASCII .GetBytes (welcome ); //发送信息 newsock .SendTo (data ,data.Length ,SocketFlags .None ,Remote ); while (true ) { data =new byte [1024]; //发送接受信息 recv =newsock.ReceiveFrom(data ,ref Remote); Console .WriteLine (Encoding .ASCII .GetString (data ,0,recv)); newsock .SendTo (data ,recv ,SocketFlags .None ,Remote ); } } } } //客户端代码 : using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; namespace UDPClient { class Program { static void Main(string[] args) { byte[] data = new byte[1024]; string input ,stringData; //构建TCP 服务器 Console.WriteLine("This is a Client, host name is {0}", Dns.GetHostName()); //设置服务IP,设置TCP端口号 IPEndPoint ip = new IPEndPoint(IPAddress .Parse ("127.0.0.1") , 8001); //定义网络类型,数据连接类型和网络协议UDP Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); string welcome = "你好! "; data = Encoding.ASCII.GetBytes(welcome); server.SendTo(data, data.Length, SocketFlags.None, ip); IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0); EndPoint Remote = (EndPoint)sender; data = new byte[1024]; //对于不存在的IP地址,加入此行代码后,可以在指定时间内解除阻塞模式限制 int recv = server.ReceiveFrom(data, ref Remote); Console.WriteLine("Message received from {0}: ", Remote.ToString()); Console.WriteLine(Encoding .ASCII .GetString (data,0,recv)); while (true) { input = Console .ReadLine (); if (input =="exit") break ; server .SendTo (Encoding .ASCII .GetBytes (input ),Remote ); data = new byte [1024]; recv = server.ReceiveFrom(data, ref Remote); stringData = Encoding.ASCII.GetString(data, 0, recv); Console.WriteLine(stringData); } Console .WriteLine ("Stopping Client."); server .Close (); } } } //2.使用UdpClient 来实现他们之间的通信服务器端代码 using System; using System.Net; using System.Net.Sockets; using System.Text; public class Custom { private static readonly IPAddress GroupAddress = IPAddress.Parse("IP地址"); private const int GroupPort = 11000; private static void StartListener() { bool done = false; UdpClient listener = new UdpClient(); IPEndPoint groupEP = new IPEndPoint(GroupAddress,GroupPort); try { listener.JoinMulticastGroup(GroupAddress); listener.Connect(groupEP); while (!done) { Console.WriteLine("Waiting for broadcast"); byte[] bytes = listener.Receive( ref groupEP); Console.WriteLine("Received broadcast from {0} :\n {1}\n", groupEP.ToString(), Encoding.ASCII.GetString(bytes,0,bytes.Length)); } listener.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static int Main(String[] args) { StartListener(); return 0; } } //客户端代码: using System; using System.Net; using System.Net.Sockets; using System.Text; public class Client { private static IPAddress GroupAddress = IPAddress.Parse("IP地址"); private static int GroupPort = 11000; private static void Send( String message) { UdpClient sender = new UdpClient(); IPEndPoint groupEP = new IPEndPoint(GroupAddress,GroupPort); try { Console.WriteLine("Sending datagram : {0}", message); byte[] bytes = Encoding.ASCII.GetBytes(message); sender.Send(bytes, bytes.Length, groupEP); sender.Close(); } catch (Exception e) { Console.WriteLine(e.ToString()); } } public static int Main(String[] args) { Send(args[0]); return 0; } }
2.TcpClient类
TCPClient类使用TCP从因特网上请求数据。TCP建立与远程终结点的连接,然后使用此连接发送和接收数据包。
TcpClient类属性
TcpClient类的方法
static void Connect(String server, String message) { try { // 创建一个TcpClient对象. // 连接到服务器指定的相同地址,端口 Int32 port = 13000; TcpClient client = new TcpClient(server, port); // 将传递的消息转换为ASCII,并将其存储为Byte数组。 Byte[] data = System.Text.Encoding.ASCII.GetBytes(message); // 获取用于读写的客户端数据流。 // Stream stream = client.GetStream(); NetworkStream stream = client.GetStream(); // 将消息写入客户端数据流 stream.Write(data, 0, data.Length); Console.WriteLine("Sent: {0}", message); // 接收返回消息 // 创建用于存储响应字节的byte数组 data = new Byte[256]; // 创建用于存储响应字节的空字符串 String responseData = String.Empty; // 读取TcpServer响应字节。 Int32 bytes = stream.Read(data, 0, data.Length); responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes); Console.WriteLine("Received: {0}", responseData); // 关闭 stream.Close(); client.Close(); } catch (ArgumentNullException e) { Console.WriteLine("ArgumentNullException: {0}", e); } catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); } Console.WriteLine("\n Press Enter to continue..."); Console.Read(); }
3.TcpListener类
TcpListener类用来侦听来自TCP网络客户端的连接。
可使用TcpClient或Socket来连接TcpListener。
可使用IPEndPoint、本地IP地址及端口号或者仅使用端口号,来创建TcpListener。
TcpListen类的属性
TcpListen类的方法
AcceptSocket() |
接受挂起的连接请求。 |
AcceptSocketAsync() |
接受挂起的连接请求以作为异步操作。 |
AcceptSocketAsync(CancellationToken) |
接受挂起的连接请求作为可取消的异步操作。 |
AcceptTcpClient() |
接受挂起的连接请求。 |
AcceptTcpClientAsync() |
接受挂起的连接请求以作为异步操作。 |
AcceptTcpClientAsync(CancellationToken) |
接受挂起的连接请求作为可取消的异步操作。 |
AllowNatTraversal(Boolean) |
启用或禁用针对 TcpListener 实例的网络地址转换 (NAT) 遍历。 |
BeginAcceptSocket(AsyncCallback, Object) |
开始一个异步操作来接受一个传入的连接尝试。 |
BeginAcceptTcpClient(AsyncCallback, Object) |
开始一个异步操作来接受一个传入的连接尝试。 |
Create(Int32) |
创建一个新的侦听指定端口的 TcpListener 实例。 |
EndAcceptSocket(IAsyncResult) |
异步接受传入的连接尝试,并创建新的 Socket 来处理远程主机通信。 |
EndAcceptTcpClient(IAsyncResult) |
异步接受传入的连接尝试,并创建新的 TcpClient 来处理远程主机通信。 |
Equals(Object) |
确定指定对象是否等于当前对象。 (继承自 Object) |
GetHashCode() |
作为默认哈希函数。 (继承自 Object) |
GetType() |
获取当前实例的 Type。 (继承自 Object) |
MemberwiseClone() |
创建当前 Object 的浅表副本。 (继承自 Object) |
Pending() |
确定是否有挂起的连接请求。 |
Start() |
开始侦听传入的连接请求。 |
Start(Int32) |
启动对具有最大挂起连接数的传入连接请求的侦听。 |
Stop() |
关闭侦听器。 |
ToString() |
返回表示当前对象的字符串。 (继承自 Object) |
using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; class MyTcpListener { public static void Main() { TcpListener server = null; try { // 设置TcpListener端口为13000. Int32 port = 13000; IPAddress localAddr = IPAddress.Parse("127.0.0.1"); // 根据IP地址创建一个新的TcpListener server = new TcpListener(localAddr, port); // 开始 server.Start(); // 存储监听数据的byte数组 Byte[] bytes = new Byte[256]; String data = null; //主线程循环监听 while (true) { // 执行循环监听 using (TcpClient client = server.AcceptTcpClient()) { NetworkStream stream = client.GetStream(); int i; //接收数据 while ((i = stream.Read(bytes, 0, bytes.Length)) != 0) { // 将接收到的数据转换成字符串 data = System.Text.Encoding.ASCII.GetString(bytes, 0, i); Console.WriteLine("Received: {0}", data); // 处理接收到的数据 data = data.ToUpper(); byte[] msg = System.Text.Encoding.ASCII.GetBytes(data); // 回复消息 stream.Write(msg, 0, msg.Length); Console.WriteLine("Sent: {0}", data); } } data = null; } } catch (SocketException e) { Console.WriteLine("SocketException: {0}", e); } finally { server.Stop(); } Console.WriteLine("\nHit enter to continue..."); Console.Read(); } }
五、根据Socket原理实现一个聊天器
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Sockets; using System.Threading; using static System.Text.Encoding; using System.Net; using System.IO; namespace Chat { /// <summary> /// 接收事件处理 /// </summary> /// <param name="type"></param> /// <param name="msg"></param> public delegate void OnReceiveEventHandler(ChatType type, string msg); /// <summary> /// 断开事件处理 /// </summary> /// <param name="e"></param> public delegate void DisconnectEventHandler(Exception e); /// <summary> /// 连接事件处理 /// </summary> public delegate void ConnectEventHandler(); /// <summary> /// 数据格式 /// </summary> public enum ChatType : byte { Str, File, } abstract class ChatBase { /// <summary> /// Socket属性 /// </summary> public Socket ConnectedSocket { get; set; } /// <summary> /// 是否断线重连 /// </summary> public bool AutoReConnect { get; set; } = true; /// <summary> /// 接收事件处理事件 /// </summary> public event OnReceiveEventHandler OnReceive; /// <summary> /// 断开事件处理事件 /// </summary> public event DisconnectEventHandler OnDisconnect; /// <summary> /// 连接事件处理事件 /// </summary> public abstract event ConnectEventHandler OnConnect; /// <summary> /// 私有Socket对象 /// </summary> private Socket socket; public string ip; public int port; public string dirName = "ChatFiles"; /// <summary> /// 构造函数 /// </summary> /// <param name="ip"></param> /// <param name="port"></param> public ChatBase(string ip, int port) { this.ip = ip; this.port = port; } /// <summary> /// 抽象方法开始服务 /// </summary> public abstract void Start(); /// <summary> /// 获取IPEndPoint /// </summary> /// <returns></returns> public IPEndPoint GetIPEndPoint() { return new IPEndPoint(IPAddress.Parse(ip), port); } /// <summary> /// 获取socket /// </summary> /// <returns></returns> public Socket GetSocket() { if(socket == null) { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } return socket; } /// <summary> /// 发送消息方法 /// </summary> /// <param name="msg">消息文本</param> /// <returns></returns> public bool Send(string msg) { ///判断socket属性是否实例化和连接到对象 if (ConnectedSocket != null && ConnectedSocket.Connected) { //将消息文本转换为buty数组 byte[] buffer = UTF8.GetBytes(msg); byte[] len = BitConverter.GetBytes((long)buffer.Length); byte[] content = new byte[1 + len.Length + buffer.Length]; //数组第一个字节标识数据格式 content[0] = (byte)ChatType.Str; Array.Copy(len, 0, content, 1, len.Length); Array.Copy(buffer, 0, content, 1 + len.Length, buffer.Length); try { //将数据发送到连接的socket ConnectedSocket.Send(content); return true; } catch (Exception e) { Console.WriteLine(e.Message + " ooo"); } } return false; } /// <summary> /// 发送文件方法 /// </summary> /// <param name="path"></param> /// <returns></returns> public bool SendFile(string path) { //判断socket属性是否实例化和连接到对象 if (ConnectedSocket != null && ConnectedSocket.Connected) { try { //将文件地址和长度信息转换为byte数组 FileInfo fi = new FileInfo(path); byte[] len = BitConverter.GetBytes(fi.Length); byte[] name = UTF8.GetBytes(fi.Name); byte[] nameLen = BitConverter.GetBytes(name.Length); byte[] head = new byte[1 + len.Length + nameLen.Length + name.Length]; //数组第一个字节标识数据格式 head[0] = (byte)ChatType.File; Array.Copy(len, 0, head, 1, len.Length); Array.Copy(nameLen, 0, head, 1 + len.Length, nameLen.Length); Array.Copy(name, 0, head, 1 + len.Length + nameLen.Length, name.Length); //将文件信息和数据缓冲区传输到socket对象 ConnectedSocket.SendFile( path, head, null, TransmitFileOptions.UseDefaultWorkerThread ); return true; } catch(Exception e) { // 连接断开了 Console.WriteLine("send file exception : " + e.Message); } } return false; } /// <summary> /// 开始接收数据处理 /// </summary> public void StartReceive() { //开启一个新的线程处理接收事件 Thread receiveThread = new Thread(new ThreadStart(Receive)); receiveThread.IsBackground = true; receiveThread.Start(); } /// <summary> /// 数据接收事件 /// </summary> public void Receive() { //判断socket属性是否未定义 if (ConnectedSocket != null) { //循环监听接收消息 while (true) { try { //接收指定字节的数据 byte[] head = new byte[9]; ConnectedSocket.Receive(head, head.Length, SocketFlags.None); //获取接收数据的长度 int len = BitConverter.ToInt32(head, 1); //根据第一个字节判断数据格式 if (head[0] == (byte) ChatType.Str) { //字符串直接调用OnReceive事件处理 byte[] buffer = new byte[len]; ConnectedSocket.Receive(buffer, len, SocketFlags.None); OnReceive(ChatType.Str, UTF8.GetString(buffer)); } else if(head[0] == (byte)ChatType.File) { //文件先判断文件存储地址是否存在,没有就新建一个文件夹 if (!Directory.Exists(dirName)) { Directory.CreateDirectory(dirName); } //前四个字节接收文件名称长度信息 byte[] nameLen = new byte[4]; ConnectedSocket.Receive(nameLen, nameLen.Length, SocketFlags.None); //根据文件长度信息获取文件名称信息 byte[] name = new byte[BitConverter.ToInt32(nameLen, 0)]; ConnectedSocket.Receive(name, name.Length, SocketFlags.None); string fileName = UTF8.GetString(name); int readByte = 0; int count = 0; byte[] buffer = new byte[1024 * 8]; //文件路径 string filePath = Path.Combine(dirName, fileName); //判断文件是否存在,存在就删除 if (File.Exists(filePath)) { File.Delete(filePath); } //使用FileStream读取缓冲区文件 using (FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write)) { //根据接收文件的长度循环读取写入信息 while (count != len) { //一次读取长度 int readLength = buffer.Length; if(len - count < readLength) { readLength = len - count; } //读取文件 readByte = ConnectedSocket.Receive(buffer, readLength, SocketFlags.None); fs.Write(buffer, 0, readByte); //记录已读取的文件长度 count += readByte; } } //接收完成后调用OnReceive事件处理结果 OnReceive(ChatType.File, fileName); } else { // 未知类型 } } catch (Exception e) { OnDisconnect(e); // 连接异常断开 if (AutoReConnect) { this.socket.Close(); this.socket = null; this.Start(); } break; } } } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net; using System.Net.Sockets; using System.Threading; using Chat; namespace Chat { class ChatServer : ChatBase { /// <summary> /// 连接事件处理事件 /// </summary> public override event ConnectEventHandler OnConnect; /// <summary> /// 构造函数 /// </summary> /// <param name="ip"></param> /// <param name="port"></param> public ChatServer(string ip, int port) : base(ip, port) { //获取本机IP地址 string localIP = string.Empty; foreach(IPAddress address in Dns.GetHostEntry(Dns.GetHostName()).AddressList) { if(address.AddressFamily.ToString() == "InterNetwork") { localIP = address.ToString(); } } this.ip = localIP; } /// <summary> /// 重写开始服务方法,服务端开启监听 /// </summary> public override void Start() { StartListen(); } /// <summary> /// 开启监听 /// </summary> public void StartListen() { //获取socket对象 Socket socket = GetSocket(); // 将套接字与IPEndPoint绑定 socket.Bind(this.GetIPEndPoint()); // 开启监听 仅支持一个连接 socket.Listen(1); //开启一个后台新线程开始监听服务 Thread acceptThread = new Thread(new ThreadStart(TryAccept)); acceptThread.IsBackground = true; acceptThread.Start(); } /// <summary> /// 监听服务 /// </summary> public void TryAccept() { //获取socket对象 Socket socket = GetSocket(); while (true) { try { Socket connectedSocket = socket.Accept(); this.ConnectedSocket = connectedSocket; OnConnect(); this.StartReceive(); break; } catch (Exception e) { } } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net; using System.Net.Sockets; using static System.Text.Encoding; using Chat; using System.Threading; namespace Chat { class ChatClient : ChatBase { public int TryConnectInterval = 2000; public override event ConnectEventHandler OnConnect; public ChatClient(string ip, int port) : base(ip, port) { } public override void Start() { StartConnect(); } public void StartConnect() { Thread connectThread = new Thread(new ThreadStart(TryConnect)); connectThread.IsBackground = true; connectThread.Start(); } public void TryConnect() { Socket socket = GetSocket(); while (true) { try { Console.WriteLine("try connect ... ..."); socket.Connect(this.GetIPEndPoint()); // 如果连接上 this.ConnectedSocket = socket; OnConnect(); this.StartReceive(); Console.WriteLine("connected ... ..." + socket.RemoteEndPoint.ToString()); break; } catch (Exception e) { Console.WriteLine("connect exception : " + e.Message); Thread.Sleep(TryConnectInterval); } } } } }
源码文件连接:https://files.cnblogs.com/files/blogs/795873/Chat-master.rar?t=1690022297&download=true