基于TCP与UDP协议的socket通信
C/S架构与初识socket 在开始socket介绍之前,得先知道一个Client端/Server端架构,也就是
计算机网络的核心就是一堆协议,想开发基于网络通信的软件就必须遵守这些协议。但是由于学习协议的代价巨大:TCP/IP等等协议就是研究生研究这玩意儿的,等你研究完了黄花菜都凉了。 那么可以不用去了解这些协议也能做到开发网络通信软件的需求吗?可以, MAC地址存在于网卡之上,是全世界唯一的标识主机位置的一种信息,而端口号则是为了区分操作系统上各个应用程序而衍生出的概念,IP地址绑定于网卡,MAC地址也绑定于网卡。那么有了IP地址 + 端口号,就能够去标识整个互联网中的一个独一无二的应用程序了。 所以: 套接字发展史 套接字,就是 套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix,它最初的设计是为了让同一台主机上的多个应用程序之间进行通信,也就是进程通信或者被称为 下面我们就来介绍这两种套接字家族。(套接字家族你可以将它理解为一种种类,反正就是一种是基于文件的,一种是基于网络的就行了。) 基于文件的套接字家族 名称: 作用:Unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来存取数据,两个套接字进程运行在同一台机器上,可以通过访问同一个文件系统间接完成通信。 基于网络的套接字家族 名称: 作用:有了IP + PORT 我们可以与互联网上的任何应用程序进行通信,这就是它的作用。除此之外还有一个叫 套接字工作流程介绍我们需要自己编写一个套接字Client端以及Server端,故应该选用基于网络的套接字家族。而其中基于TCP协议的套接字工作流程与基于UDP协议的套接字工作流程又不一样。 基于TCP协议的套接字工作流程图由于TCP协议本身比较复杂,故使用基于TCP协议的套接字编写程序整体流程也较为复杂。 基于UDP协议的套接字工作流程图基于UDP协议的套接字工作流程相比于基于TCP协议的套接字工作流程来说简单一些,因为不用建立双向链接通道。 TCP协议TCP协议是一种基于字节流的形式,什么叫流呢?其实就是像水龙头一样打开哗啦啦的没有确切的边界,这个就叫流。 TCP协议会去创建一个双向链接通道,用于收发消息,如图: 要去建立这个通道必须是要经历三次握手,要关闭这个通道也必须经历四次挥手,没有这个通道,Server端与Client端就无法正常通信。 此外TCP协议还有一个别称叫做好人协议,这个在下面章节中会做详细介绍。 TCP协议报文格式先不急介绍三次握手啊,双向链接通道这些玩意儿。在研究这两个东西之前我们先要看一下TCP协议的报文格式。(着重看一下ACK与SYN)
TCP报文是TCP层传输的数据单元,也叫报文段。 1、端口号:用来标识同一台计算机的不同的应用进程。 1)源端口:源端口和IP地址的作用是标识报文的返回地址。 2)目的端口:端口指明接收方计算机上的应用程序接口。 TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。 2、序号和确认号:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。 3、数据偏移/首部长度:4bits。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何任选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*32/8 = 60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。 4、保留:为将来定义新的用途保留,现在一般置0。 5、控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。 1)URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。 2)ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。 3)PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。 4)RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。 5)SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。 6)FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。 6、窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小时一个16bit字段,因而窗口大小最大为65535。 7、校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。 8、紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。 9、选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。 10、数据部分: TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。 TCP协议之三次握手上面我们说过,Server端与Client端想要进行通信,必须要经历三次握手这么一个流程。它的图示如下: SYN_SENT:Client端发送一次建立链接请求且没有收到Server端回应时会进入该状态。Linux操作系统下可用 ESTABLISHED:当某一方进入该状态,则代表可以向另一方发送数据了。 LISTEN:Server端在等待Client端建立三次握手的连接时会进入该状态。 SYN_RCVD:Server端进入该状态代表已收到ClientClient端的三次握手链接请求。并回复了SYN以及ACK SYN: 建立链接的标志位 ACK:确认请求的标志位 seq: 可以理解为一段暗号,用于确认该信息未被修改。 上图Client端发送了一个SYN请求,而Server端则回应了一个ACK并且在原有的x上加了一个1,Client端收到后就知道Server端允许建立链接且该信息未被中间篡改,此时Client端就进入ESTABLISHED状态,一旦进入这个状态代表链接通道已建立好,Client端可以给Server端发送消息了。 此外,Server端还给Client端发送了一个SYN请求,并且附带seq是y,Client端就知道原来Server端也想要和自己建立一个链接通道,于是回复ACK = y + 1,当Server端读到该消息依旧是具有两层含义。 1.这段消息未被修改 2.y+1代表我同意你的这条请求
可靠传输协议的由来TCP协议为何被称为可靠传入协议是有原因的,如下图:(三次握手时的数据交互并不是走双向链接通道,而对于下图的数据传输来说则是走的双向链接通道了。)
TCP协议之四次挥手为什么创建链接需要3步,而断开链接则需要4步呢? 可以看到,三次握手之前是没有数据传输的,并且其中第二次是一次性发送了一个请求和一个确认。所以减少了一次操作。而四次挥手涉及到数据的传输,所以不可能简化成三次挥手。(四次挥手也是不同于三次握手,四次挥手也是建立在双向链接通道的基础之上的,而三次握手的时候该双向通道还未建立成功) FIN_WAIT_1:代表主动发起断开链接请求 FIN_WAIT_2:代表此时的Client端不会再主动向Server端发送数据 TIME_WAIT:代表Client端还要回复最后一条确认消息,回复完毕后双向链接正式关闭 CLOSE_WAIT:代表关闭等待 LAST_ACK:代表持续的确认(即只要Client端没有回复第4条信息,Server端就不断尝试发送断开链接的FIN请求) 请记住:在实际生活场景中,服务端主动断开链接的情况比较多,因为它涉及到了和很多客户端的通信,还有的客户端还在排队,所以不可能对一个客户端浪费太多时间。这句话你可以理解为: 服务器是个渣男 ,很多女孩子(Client端)都喜欢他,都给他写情书,他回复完了一个女孩子的情书后立马会拆开下一封情书,并不会只留恋于一封。 UDP协议UDP协议是一种基于数据报的格式(也被称为基于消息),不同于TCP的字节流格式。UDP的数据报格式是有头有尾的,这一点很重要。对应下图: 另外UDP协议的数据传输是不需要建立双向链接通道的,并且UDP发消息与TCP不太一样。它发一次就不会管了,不管对方有没有收到都不会再发,所以这也是UDP协议被称为不可靠传输协议的由来。 基于TCP协议的socket简单通信我们决定在两台机器上进行套接字通信。本机作为Client端,而云端服务器作为Server端,整个过程先从流程图开始一步一步的进行实验。 服务器信息如下: [root@tencent-server MySocketServer]# uname -a Linux tencent-server 3.10.0-862.el7.x86_64 1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux [root@tencent-server MySocketServer] cat /proc/version Linux version 3.10.0-862.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC) ) 1 SMP Fri Apr 20 16:44:24 UTC 2018 客户机信息如下: C:UsersAdministrator>systeminfo 主机名: DESKTOP-BTUC3PT OS 名称: Microsoft Windows 10 专业工作站版 OS 版本: 10.0.18363 暂缺 Build 18363 OS 制造商: Microsoft Corporation OS 配置: 独立工作站 OS 构建类型: Multiprocessor Free 注册的所有人: Windows User 注册的组织: P R C 产品 ID: 00391-90134-77505-AA010 初始安装日期: 2020/5/6,13:23:01 系统启动时间: 2020/6/20,0:55:40 系统制造商: Shinelon Computer 系统型号: TN15S 系统类型: x64-based PC 处理器: 安装了 1 个处理器。 [01]: Intel64 Family 6 Model 60 Stepping 3 GenuineIntel ~2801 Mhz BIOS 版本: American Megatrends Inc. 1.04,2016/1/26 Windows 目录: C:Windows 系统目录: C:Windowssystem32 启动设备: DeviceHarddiskVolume1 系统区域设置: zh-cn;中文(中国) 输入法区域设置: zh-cn;中文(中国) 时区: (UTC+08:00) 北京,重庆,香港特别行政区,乌鲁木齐 物理内存总量: 8,079 MB 可用的物理内存: 2,687 MB 虚拟内存: 最大值: 10,827 MB 虚拟内存: 可用: 3,257 MB 虚拟内存: 使用中: 7,570 MB 页面文件位置: C:pagefile.sys 域: WORKGROUP 登录服务器: DESKTOP-BTUC3PT 修补程序: 安装了 10 个修补程序。 [01]: KB4552931 [02]: KB4513661 [03]: KB4516115 [04]: KB4517245 [05]: KB4528759 [06]: KB4537759 [07]: KB4552152 [08]: KB4560959 [09]: KB4561600 [10]: KB4560960 网卡: 安装了 3 个 NIC。 [01]: Realtek RTL8723AE Wireless LAN 802.11n PCI-E NIC 连接名: WLAN 启用 DHCP: 是 DHCP 服务器: 192.168.1.1 IP 地址 [01]: 192.168.1.103 [02]: fe80::b53b:15ba:3b3d:2a2 [02]: Realtek PCIe GBE Family Controller 连接名: 以太网 状态: 媒体连接已中断 [03]: Bluetooth Device (Personal Area Network) 连接名: 蓝牙网络连接 状态: 媒体连接已中断 Hyper-V 要求: 虚拟机监视器模式扩展: 是 固件中已启用虚拟化: 是 二级地址转换: 是 数据执行保护可用: 是 Server端代码如下: !/usr/bin/env python3 # -*- coding:utf-8 -*- ==== 基于TCP协议的socket通信之Server ==== Client端代码如下: ==== 基于TCP协议的socket通信之Client ==== 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,1)">socket.SOCK_STREAM) 2. 发送请求链接 client.connect((xxx.xxx.xxx.xxx 设置为服务器公网IP 3. 开始通信 client.send(hello,world".encode(utf-8")) print(client.recv(1024).decode()) 4. 关闭客户机 client.close() 先运行Server端,再运行Client端。得到以下结果 可以看到我们的消息成功的发送回来了。实验成功! 增加双层循环 我们的Server端在将信息做了一个 这个时候我们将测试环境搬回到本地。并对代码做出一些改进: Server端改进代码如下: ==== 基于TCP协议的socket通信之Server ==== 127.0.0.1 4.2 client_addr: 服务端地址信息 改进1:服务端能够不断的处理客户端发来的请求 while 1: ) conn.send(data.upper()) 8.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close() Client端改进代码如下: 改进1:我们可以自行的发送任何想发的数据 : message = input(>>>).strip() 3. 开始通信 client.send(message.encode()) 4.关闭通信 client.close() 这个时候我们就可以源源不断的给Server端发送消息,而不是发送一次就结束了。 还有一个问题,即我们的Server端只能接受一个用户,这显然太low了,有什么好的解决方案吗?暂时没有。因为还没学习多线程相关知识,所以我们只能退而求其次的对Server端多增加一个外层循环,用来源源不断的与不同的Client端建立双向链接通道。(非并发性的,可以将它称之为链接循环) Server端代码如下: 改进2:可以让服务端接收多个客户端发送的建立双向链接通道的请求(非并发性) 4. 阻塞等待三次握手请求 conn,1)"> server.accept() 4.1 conn:双向链接通道 4.2 client_addr: 服务端地址信息 改进1:服务端能够不断的处理客户端发来的请求 : ) conn.send(data.upper()) 8.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close() Server端异常崩溃的BUG如果你认为上面的代码已经初具雏形,那么就大错特错了。如果你按照以下的步骤进行操作会发现Server端会异常终止掉:
Traceback (most recent call last): File C:/Users/Administrator/PycharmProjects/learn/服务端.pyin <module> 改进1:服务端能够不断的处理客户端发来的请求 ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。 这是为什么呢?因为这个链接通道是双向的,一方关闭链接通道后这个链接通道就会崩塌。从而导致Server端发生异常,并且这种异常在不同的平台之下还有不同的表现形式:
如何解决?方式很简单。添加上 4.2 client_addr: 服务端地址信息 try: bug修复:针对windows环境 ) if not data: bug修复:针对类UNIX环境 break conn.send(data.upper()) except ConnectionResetError as e: print(client_addr,关闭了双向链接break 7.关闭双向通道(释放占用的系统资源,因为底层都是由操作系统操作,由于双向链接通道已经断开。所以这里我们也将此双向链接进行关闭,否则就会一直占用系统资源) conn.close() 8.关闭服务器(释放Python应用程序占用的内存资源,可选,该句可以删除。因为毕竟Server端一般情况下不会关闭) server.close() Server端异常崩溃的BUG如果你认为上面的代码已经初具雏形,那么就大错特错了。如果你按照以下的步骤进行操作会发现Server端会异常终止掉:
这是为什么呢?因为这个链接通道是双向的,一方关闭链接通道后这个链接通道就会崩塌。从而导致Server端发生异常,并且这种异常在不同的平台之下还有不同的表现形式:
如何解决?方式很简单。添加上
Client端发送空会卡住的BUG我们的Server端已经优化完毕了,但是Client端还有一个BUG没解决。尝试用以下步骤就可以触发该BUG
可以发现此时的Client端进入了
了解了底层原理后,我们看一下解决方案。其实只要设置成不让Client端发送空消息即可,也就是一个 -*- coding:utf-8 -*- ==== 基于TCP协议的socket通信之Client ==== 设置为服务器公网IP not message: bug修复:针对输入空消息会卡住的情况 continue if message == quit": 改进2:用户输入quit会断开链接 4.关闭通信 client.close() 基于UDP协议的socket简单通信我们依然将测试环境放在本机。并按照基于UDP协议的套接字工作流程图进行代码的编写。
Server端代码如下: ==== 基于UDP协议的socket通信之Server ==== socket.SOCK_DGRAM) 3.获取到收发消息的内容以及其IP地址 data,client_addr = server.recvfrom(1024 4.发消息 server.sendto(data.upper(),client_addr) 5.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close() Client端代码如下: ==== 基于UDP协议的socket通信之Client ==== 2. 发送数据 client.sendto("),( 3. 读取数据 data,server_addr = client.recvfrom(1024) print(data.decode( 4.关闭通信 client.close() 增加单层循环由于基于UDP协议通信不会建立双向链接通道,所以我们只需要增加一个通信循环即可。 Server端改进代码如下: while 1: 改进1:增加通信循环 3.获取到收发消息的内容以及其IP地址 data,1)">) server.sendto(data.upper(),1)"> # 5.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close() Client端改进代码如下: 2. 发送数据 client.sendto(message.encode( 3. 读取数据 data,1)"> 4.关闭通信 client.close() BUG测试我们对该两段代码进行BUG测试均为发现异常。
解决端口占用问题 在进行 加入一条socket配置,重用ip和端口 from socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) 就是它,在bind前加 server.bind(('',6666)) 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决, vi / etc / sysctl.conf 编辑文件,加入以下内容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 然后执行 / sbin / sysctl - p 让参数生效。 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME - WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME - WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间 扩展:socket全方法详解
?扩展:pwd寄存器与用户态内核态有一个名叫psw的寄存器就是区分内核态和用户态的,它有2个状态位,当CPU指令集是0的时候对应到内核态,也就获取了所有的内存权限。当指令集是1的时候对应到用户态,保留一部分内存不让访问。所以说真正的内存是不可划分的,都只是一个状态不同的问题。 当应用层面的程序被CPU执行时,那么可以肯定的是它的状态必定是1,限制了一些调度硬件的权限。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |