C/S架构与初识socket
在开始socket介绍之前,得先知道一个Client端/Server端架构,也就是 C/S
架构,互联网中处处充满了 C/S
架构(Client/Server),比如我们需要玩英雄联盟,就必须连接至英雄联盟的服务器上,那么对于我们玩家来说它的英雄联盟服务器就是Server端,而我们必须要有一个英雄联盟Client端才能够去和英雄联盟Server端进行数据交互。
TCP/IP五层网络模型介绍 | |
---|---|
层级 | 功能 |
应用层 | 跑应用协议的,如:HTTP,FTP等等,主要职责便是规定应用数据的格式。可以自定义协议,但是必须要有head部分与data部分。 |
传输层 | 跑端口协议的,如:TCP / UDP等等,主要职责便是用于区分该系统上的唯一一个网络应用程序。 |
网络层 | IP地址子网掩码等等相关都在网络层,如:IP协议,主要职责便是用来区分广播域,防止网络风暴的发生。 |
数据链路层 | 划分电信号以及IP地址与MAC地址相互转换,如:以太网协议,ARP协议等等,用来区分电信号与支持通信的。 |
物理层 | 传输电信号,网络数据传输的基石。 |
计算机网络的核心就是一堆协议,想开发基于网络通信的软件就必须遵守这些协议。但是由于学习协议的代价巨大:TCP/IP等等协议就是研究生研究这玩意儿的,等你研究完了黄花菜都凉了。
那么可以不用去了解这些协议也能做到开发网络通信软件的需求吗?可以,socket
提供了这一可能性,socket
位于应用层和传输层之间,也就相当于加了一层socket
抽象层,它向下封装了各种协议,用户只需要通过socket
提供的接口就能完成该需求,而并不需要深入的去研究某些协议。比如(TCP,UDP)等等...
MAC地址存在于网卡之上,是全世界唯一的标识主机位置的一种信息,而端口号则是为了区分操作系统上各个应用程序而衍生出的概念,IP地址绑定于网卡,MAC地址也绑定于网卡。那么有了IP地址 + 端口号,就能够去标识整个互联网中的一个独一无二的应用程序了。
所以:socket
也被人称为 ip + port...
套接字发展史
套接字,就是socket
,由于进程中本身是不允许通信的,但是可以通过套接字来发送或者接受数据,可以对其进行像对文件一样的打开,读写,和关闭操作。并且套接字允许应用程序将I/O(输入输出)插入到网络中,并与网络中的其他应用程序进行通信,基于网络的套接字就是IP地址加端口的组合(ip + port)
套接字起源于20世纪70年代加利福尼亚大学伯克利分校版本的Unix,它最初的设计是为了让同一台主机上的多个应用程序之间进行通信,也就是进程通信或者被称为IPC
,套接字有两种(基于文件,基于网络)
下面我们就来介绍这两种套接字家族。(套接字家族你可以将它理解为一种种类,反正就是一种是基于文件的,一种是基于网络的就行了。)
基于文件的套接字家族
名称: AF_UNIX
作用:Unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来存取数据,两个套接字进程运行在同一台机器上,可以通过访问同一个文件系统间接完成通信。
基于网络的套接字家族
名称: AF_INET
作用:有了IP + PORT 我们可以与互联网上的任何应用程序进行通信,这就是它的作用。除此之外还有一个叫 AF_INET6
的玩意儿,也就是基于IPV6的东西,AF_INET
是IPV4,目前广泛采用。除此之外还有许多成员,不做过多介绍。对了,忘记说一点很重要的概念:
套接字工作流程介绍
我们需要自己编写一个套接字Client端以及Server端,故应该选用基于网络的套接字家族。而其中基于TCP协议的套接字工作流程与基于UDP协议的套接字工作流程又不一样。
基于TCP协议的套接字工作流程图
由于TCP协议本身比较复杂,故使用基于TCP协议的套接字编写程序整体流程也较为复杂。
基于UDP协议的套接字工作流程图
基于UDP协议的套接字工作流程相比于基于TCP协议的套接字工作流程来说简单一些,因为不用建立双向链接通道。
TCP协议
TCP协议是一种基于字节流的形式,什么叫流呢?其实就是像水龙头一样打开哗啦啦的没有确切的边界,这个就叫流。
TCP协议会去创建一个双向链接通道,用于收发消息,如图:
要去建立这个通道必须是要经历三次握手,要关闭这个通道也必须经历四次挥手,没有这个通道,Server端与Client端就无法正常通信。
此外TCP协议还有一个别称叫做好人协议,这个在下面章节中会做详细介绍。
TCP协议报文格式
先不急介绍三次握手啊,双向链接通道这些玩意儿。在研究这两个东西之前我们先要看一下TCP协议的报文格式。(着重看一下ACK与SYN)
TCP协议中的六个标志分别是,URG、ACK、PSH、RST、SYN、FIN。
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操作系统下可用netstat命令
查看当前状态。一般来说该状态持续时间非常短,几乎不可测。
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代表我同意你的这条请求
SYN洪水攻击:
当Server端长期进入SYN_RCVD状态时就要当心是否遭受了SYN洪水攻击。因为TCP三次握手对于Server端来讲会无限的回复Client端发来的SYN请求,收到一条就回一条。如果有黑客模拟成千上万台Client端对Server端发送SYN请求在发送第一次握手后就溜溜球了那么服务器还傻乎乎的等第三次的握手回信,这么做会让Server端的压力很大。所以TCP协议也被称为好人协议...
半链接池backlog:
服务器如果一次性收到很多的请求,它无法做到同时都回应这么多。就进行排队机制,将先来的请求放到backlog里,后面的就慢慢等呗,就相当于你在和你女朋友打电话的时候(backlog为1),你的好哥们儿们给你打电话让你开黑上网了。
那对于你的好哥们儿们来说就是 ---> 对不起,请不要挂机,你拨打的电话正在通话中
这对应到网络上,就是半链接池外的请求 ---> 等待服务器的回应 (SYN请求和ACK确认)即,你想要建立双向链接通道?再等等。
防止SYN 洪水攻击的有效策略其中一点就是:增大backlog链接池的最大数量(一般不用次策略)
或者也可以:缩小Server端对每个请求的返回次数(如果Server端发现Client端没理自己,就会不断的回应上次的信息。初始值为5s,过5s发一次,然后变成3s,再过3s发一次,变成1s,再发一次...直到不想发了就不会理睬这个请求了。)
平常打开一个网页打不开的时候,有一种可能性就是人家的backlog满了,你就只能排在外边儿等
可靠传输协议的由来
TCP协议为何被称为可靠传入协议是有原因的,如下图:(三次握手时的数据交互并不是走双向链接通道,而对于下图的数据传输来说则是走的双向链接通道了。)
UDP协议则没有这种确认的机制,对于安全性来说下降了不少但是对于速度上有了明显的提示。故DHCP服务以及DNS域名解析都是使用UDP协议,因为它速度更快。
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 ====
import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.绑定IP地址与PORT端口号 server.bind(("172.17.0.16",6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 # 3. 设置半连接池,代表最大有5个可以等待建立三次握手的Client端 server.listen(5) # 4. 阻塞等待三次握手请求 conn,client_addr = server.accept() # 4.1 conn:双向链接通道 # 4.2 client_addr: 服务端地址信息 # 5. 收消息,1024代表一次性读取1024字节。 data = conn.recv(1024) # 6.发消息 conn.send(data.upper()) # 7.关闭双向通道(释放占用的系统资源,因为底层都是由操作系统操作) conn.close() # 8.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close()
Client端代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Client ==== import socket # 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 发送请求链接 client.connect(("xxx.xxx.xxx.xxx",6666)) # 设置为服务器公网IP # 3. 开始通信 client.send("hello,world".encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4. 关闭客户机 client.close()
先运行Server端,再运行Client端。得到以下结果
可以看到我们的消息成功的发送回来了。实验成功!
增加双层循环
我们的Server端在将信息做了一个upper()
处理后就关闭了,这显然不符合逻辑所以我们需要为它增加一个循环(可以称之为通信循环)让它能不断的进行处理信息而不是只处理一次就关闭运行。
这个时候我们将测试环境搬回到本地。并对代码做出一些改进:
Server端改进代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Server ==== import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.绑定IP地址与PORT端口号 server.bind(("127.0.0.1",6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 # 3. 设置半连接池,代表最大有5个可以等待建立三次握手的Client端 server.listen(5) # 4. 阻塞等待三次握手请求 conn,client_addr = server.accept() # 4.1 conn:双向链接通道 # 4.2 client_addr: 服务端地址信息 # 改进1:服务端能够不断的处理客户端发来的请求 while 1: # 5. 收消息,1024代表一次性读取1024字节。 data = conn.recv(1024) # 6.发消息 conn.send(data.upper()) # 7.关闭双向通道(释放占用的系统资源,因为底层都是由操作系统操作) conn.close() # 8.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close()
Client端改进代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Client ==== import socket # 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 发送请求链接 client.connect(("127.0.0.1",6666)) # 设置为服务器公网IP # 改进1:我们可以自行的发送任何想发的数据 while 1: message = input(">>>").strip() # 3. 开始通信 client.send(message.encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4.关闭通信 client.close()
这个时候我们就可以源源不断的给Server端发送消息,而不是发送一次就结束了。
还有一个问题,即我们的Server端只能接受一个用户,这显然太low了,有什么好的解决方案吗?暂时没有。因为还没学习多线程相关知识,所以我们只能退而求其次的对Server端多增加一个外层循环,用来源源不断的与不同的Client端建立双向链接通道。(非并发性的,可以将它称之为链接循环)
Server端代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Server ==== import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 2.绑定IP地址与PORT端口号 server.bind(("127.0.0.1", 6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 # 3. 设置半连接池,代表最大有5个可以等待建立三次握手的Client端 server.listen(5) # 改进2:可以让服务端接收多个客户端发送的建立双向链接通道的请求(非并发性) while 1: # 4. 阻塞等待三次握手请求 conn, client_addr = server.accept() # 4.1 conn:双向链接通道 # 4.2 client_addr: 服务端地址信息 # 改进1:服务端能够不断的处理客户端发来的请求 while 1: # 5. 收消息,1024代表一次性读取1024字节。 data = conn.recv(1024) # 6.发消息 conn.send(data.upper()) # 7.关闭双向通道(释放占用的系统资源,因为底层都是由操作系统操作) conn.close() # 8.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close()
Server端异常崩溃的BUG
如果你认为上面的代码已经初具雏形,那么就大错特错了。如果你按照以下的步骤进行操作会发现Server端会异常终止掉:
1.开启Server端运行服务
2.开启Client端与Server端进行通信
3.停止Client端的运行,异常出现。
Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/服务端.py", line 22, in <module> # 改进1:服务端能够不断的处理客户端发来的请求 ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
这是为什么呢?因为这个链接通道是双向的,一方关闭链接通道后这个链接通道就会崩塌。从而导致Server端发生异常,并且这种异常在不同的平台之下还有不同的表现形式:
类UNIX平台下:Server端的
recv()
会无限收到空Windows平台下: Server端直接抛出
ConnectionResetError
的异常
如何解决?方式很简单。添加上try
与except
捕捉该异常,并且做一个if判断。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Server ==== import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.绑定IP地址与PORT端口号 server.bind(("127.0.0.1",6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 # 3. 设置半连接池,代表最大有5个可以等待建立三次握手的Client端 server.listen(5) # 改进2:可以让服务端接收多个客户端发送的建立双向链接通道的请求(非并发性) while 1: # 4. 阻塞等待三次握手请求 conn,client_addr = server.accept() # 4.1 conn:双向链接通道 # 4.2 client_addr: 服务端地址信息 # 改进1:服务端能够不断的处理客户端发来的请求 while 1: try: # bug修复:针对windows环境 # 5. 收消息,1024代表一次性读取1024字节。 data = conn.recv(1024) if not data: # bug修复:针对类UNIX环境 break # 6.发消息 conn.send(data.upper()) except ConnectionResetError as e: print(client_addr, "关闭了双向链接") break # 7.关闭双向通道(释放占用的系统资源,因为底层都是由操作系统操作,由于双向链接通道已经断开。所以这里我们也将此双向链接进行关闭,否则就会一直占用系统资源) conn.close() # 8.关闭服务器(释放Python应用程序占用的内存资源,可选,该句可以删除。因为毕竟Server端一般情况下不会关闭) server.close()
Client端发送空会卡住的BUG
我们的Server端已经优化完毕了,但是Client端还有一个BUG没解决。尝试用以下步骤就可以触发该BUG
1.开启Server端运行服务
2.开启Client端与Server端进行通信
3.Client端直接敲出回车(代表发出一个空)
可以发现此时的Client端进入了recv()
状态,而Server端也还是recv()
状态,这说明一个问题。该消息根本没能发出去,那么到底是为什么会有这个bug呢?我们得从其底层原理说起。
其实不管是
send()
还是recv()
都是socket
应用程序对操作系统发出一次系统调用。在此期间CPU工作状态会从用户态转变至内核态,而用户态的内存数据是不能直接与内核态的内存数据发生交互的,所以只能靠一种映射关系(可以理解为拷贝,但是并不准确)来映射出需要发送的内容。如果Client端输入一个回车,那么对于内核态中的内核缓冲区来说是接收不到该数据的。其表现形式为:
1.socket应用程序认为自己的回车(空消息)已经发送出去了
2.但实际上底层的内核缓冲区并没有将这则空消息映射出来也就造成了其实并未发送任何数据
另外,关于消息的收发其实是涉及到
队列
的概念,即先进先出。Ps:下面这幅图这样画可以便于理解,但是socket应用程序应该是在调用某项系统接口后才会如此,另外这种映射关系更确切的说其实是这样的:你
send()
什么消息不用给我内核(事实上也给不了),我内核知道自己生成这些数据。反之recv()
同理
了解了底层原理后,我们看一下解决方案。其实只要设置成不让Client端发送空消息即可,也就是一个if
判断能解决的事儿。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于TCP协议的socket通信之Client ==== import socket # 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 发送请求链接 client.connect(("127.0.0.1",6666)) # 设置为服务器公网IP # 改进1:我们可以自行的发送任何想发的数据 while 1: message = input(">>>").strip() if not message: # bug修复:针对输入空消息会卡住的情况 continue if message == "quit": # 改进2:用户输入quit会断开链接 break # 3. 开始通信 client.send(message.encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4.关闭通信 client.close()
基于UDP协议的socket简单通信
我们依然将测试环境放在本机。并按照基于UDP协议的套接字工作流程图进行代码的编写。
在使用UDP协议进行编程之前,首先要知道一件事情。
由于UDP是没有双向链接,所以首先启动任意一端都不会报错。
Server端代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于UDP协议的socket通信之Server ==== import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2.绑定IP地址与PORT端口号 server.bind(("127.0.0.1",6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 # 3.获取到收发消息的内容以及其IP地址 data,client_addr = server.recvfrom(1024) # 4.发消息 server.sendto(data.upper(),client_addr) # 5.关闭服务器(释放Python应用程序占用的内存资源,可选) server.close()
Client端代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于UDP协议的socket通信之Client ==== import socket # 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2. 发送数据 client.sendto("hello,word".encode("utf-8"),("127.0.0.1",6666)) # 3. 读取数据 data,server_addr = client.recvfrom(1024) print(data.decode("utf-8")) # 4.关闭通信 client.close()
增加单层循环
由于基于UDP协议通信不会建立双向链接通道,所以我们只需要增加一个通信循环即可。
Server端改进代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于UDP协议的socket通信之Server ==== import socket # 1.实例化socket对象 # SOCKET_DGRAM为UDP协议,SOCKET_STREAM为TCP协议 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2.绑定IP地址与PORT端口号 server.bind(("127.0.0.1",6666)) # 127.0.0.1 为回环地址,用于测试时使用。而我们在远程环境下则使用0.0.0.0 while 1: # 改进1:增加通信循环 # 3.获取到收发消息的内容以及其IP地址 data,client_addr = server.recvfrom(1024) # 4.发消息 server.sendto(data.upper(),client_addr) # # 5.关闭服务器(释放Python应用程序占用的内存资源,可选) # server.close()
Client端改进代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基于UDP协议的socket通信之Client ==== import socket # 1. 实例化socket对象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 改进1:我们可以自行的发送任何想发的数据 while 1: message = input(">>>").strip() if message == "quit": # 改进2:用户输入quit会断开链接 break # 2. 发送数据 client.sendto(message.encode("utf-8"),("127.0.0.1",6666)) # 3. 读取数据 data,server_addr = client.recvfrom(1024) print(data.decode("utf-8")) # 4.关闭通信 client.close()
BUG测试
我们对该两段代码进行BUG测试均为发现异常。
1.强制停止Client端是否会导致Server端异常崩溃?
没有导致,原因是因为UDP协议的通信不基于双向链接通道。
2.客户端发送回车或者任意空消息是否会导致
recvfrom()
卡住?没有导致,这个还是要从UDP的数据格式说起,因为UDP是数据报格式的发送,所以即便消息体是空,也还有一个消息头在里面。所以UDP的整段数据是不可能为空的,也就不会导致内核缓冲区读不到数据而卡住。
解决端口占用问题
在进行socket
编程中肯定会遇到端口被占用的情况,实际上就是服务器再向客户端发送最后一条ACK回应,也就是四次挥手中的第四步。此时服务器的状态应该处于:TIME_WAIT(等待一段时间确保双向链接通道中的信息全部读取完毕)。这是属于正常情况,请勿惊慌。解决方式如下:
#加入一条socket配置,重用ip和端口 from socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 server.bind(('127.0.0.1',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全方法详解
函数 | 描述 |
---|---|
服务器端套接字 | |
s.bind() | 绑定地址(host,port)到套接字, 在AF_INET 下,以元组(host,port)的形式表示地址。 |
s.listen() | 开始TCP监听。backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。 |
s.accept() | 被动接受TCP客户端连接,(阻塞式)等待连接的到来 |
客户端套接字 | |
s.connect() | 主动初始化TCP服务器连接,。一般address 的格式为元组(hostname,port),如果连接出错,返回socket.erro r错误。 |
s.connect_ex() | connect() 函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数 | |
s.recv() | 接收TCP数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。 |
s.send() | 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。 |
s.sendall() | 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None ,失败则抛出异常。 |
s.recvfrom() | 接收UDP数据,与recv() 类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 |
s.sendto() | 发送UDP数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。 |
s.close() | 关闭套接字 |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
s.getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值。 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。值为None 表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect() ) |
s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None 。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果flag 为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv() 没有发现任何数据,或send() 调用无法立即发送数据,那么将引起socket.error 异常。 |
s.makefile() |
扩展:pwd寄存器与用户态内核态
有一个名叫psw的寄存器就是区分内核态和用户态的,它有2个状态位,当CPU指令集是0的时候对应到内核态,也就获取了所有的内存权限。当指令集是1的时候对应到用户态,保留一部分内存不让访问。所以说真正的内存是不可划分的,都只是一个状态不同的问题。
当应用层面的程序被CPU执行时,那么可以肯定的是它的状态必定是1,限制了一些调度硬件的权限。