Python网络编程
1 基础知识
1.1 OSI七层模型与TCP/IP五层模型
1.1.1 OSI七层模型
OSI 七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,其最主要的功能就是帮助不同类型的主机实现数据传输 。
OSI七层模型从下到上依次为:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
- 物理层:以二进制数据形式在物理介质上传输数据,传输的为比特流。
- 数据链路层:将比特组合成字节,然后将字节组合成帧,使用数据链路层地址,也就是MAC地址来访问介质,并进行差错检验。该层传输的数据单位称为帧(Frame)。
- 网络层:该层主要完成的任务为IP选址以及路由的选择。该层传输的数据单位称为数据包(数据分组,Packet)。
- 传输层:建立主机端到端的连接,该层的主要作用为为上层提供端到端的可靠和透明的数据传输服务,包括差错处理和流量控制,该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。TCP协议和UDP协议均在这一层。该层传输的数据单位称为数据段(Segment)。
- 会话层:该层主要是负责建立、管理和终止表示层实体之间的通信会话。
- 表示层:该层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。
- 应用层:该层是OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。
参考自博客
1.1.2 TCP/IP五层模型
与OSI七层模型相比,TCP/IP五层模型模型将OSI七层模型中的会话层、表示层和应用层合并为一个层叫应用层。有的资料中我们会看到TCP/IP四层模型,它在五层的基础上又将物理层和数据链路层合并为了一层称为网络接口层。
TCP/IP层 | 协议 |
---|---|
应用层 | HTTP、HTTPS、DNS、FTP、SMTP、Telnet、SNMP |
传输层 | TCP、UDP |
网络层 | IP、ICMP |
网络接口层 | ARP、RARP、Ethernet |
1.2 TCP与UDP
1.2.1 TCP协议
TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。TCP通过检验和,序列号,确认应答,重发控制,连接管理以及窗口控制等机制实现可靠性传输。
TCP提供面向连接的通信传输。面向连接是指在数据通信开始之前先做好通信两端之间的准备工作。
建立TCP需要三次握手才能建立,而断开连接则需要四次握手。
-
三次握手:建立TCP连接时,需要客户端和服务端共发送3个包
- 第一次:客户端发送初始序号seq=x和SYN=1请求标志
- 第二次:服务端发送请求标志SYN=1,发送确认标志ACK=1,发送序列号seq=y,发送客户端的确认序号ack=x+1。
- 第三次:客户端发送确认标志ACK=1,发送序列号seq=x+1,发送对方的确认信号ack=y+1。
这样之后就建立成功了TCP连接,可以开始数据传输了。
-
四次挥手:
- 第一次:客户端发送断开请求,发送FIN=1标志以及序列号seq=u。
- 第二次:服务端收到后,发送ACK=1标志,确认号ack=u+1,以及自己的序列号v,进入CLOSE WAIT状态。
- 第三次:客户端收到服务端的确认结果后,进入FIN WAIT 2状态。此时服务端发送释放信号FIN=1,确认标志ACK=1,确认序列号ack=u+1,自己的序列号seq=w,然后进入LAST ACK状态。
- 第四次:客户端收到后回复,发送确认ACK=1,ack=w+1,自己的序列号seq=u+1,客户端进入TIME WAIT状态,客户端经过两个最长报文段寿命后,客户端CLOSED,服务端收到后立即进入CLOSED状态。
1.2.2 UDP协议
UDP(User Datagram Protocol,用户数据报协议)也是传输层协议,它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去。其特点如下:
- UDP是无连接的;
- UDP只是尽最大努力的交付,不保证可靠交付;
- UDP是面向报文的;
- UDP没有拥塞控制,因此网络中出现拥塞也不会使源主机的发送速率降低;
- UDP支持一对一、一对多、多对一以及多对多的交互通信;
- UDP的首部开销小,只有8个字节,比TCP的20个字节首部要短。
1.2.3 面试常见问题
-
为什么是三次握手?为什么不是两次或四次?
防止失效的连接请求报文段被服务端接收,从而产生错误。
如果进行了两次握手之后就确认了连接,而这个请求报文可能是已经失效的一个报文,如果此时建立的连接,而迟迟不能接收到客户端发送来的数据,会造成资源的浪费。那么为什么不是四次呢?因为在三次握手之后双方都知道对方的确实存在,对于第三次的握手只需要在后续数据传输中捎带确认就可以了,所以第四次握手是不需要的。
-
为什么是四次挥手?
TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP是全双工模式进行工作。当主机 1 发出 FIN 报文段时,只是表示主机 1 已经没有数据要发送了,主机 1 告诉主机 2 ,它的数据已经全部发送完毕了;但是,这个时候主机 1 还是可以接受来自主机 2 的数据;当主机 2 返回 ACK 报文段时,表示它已经知道主机 1 没有数据发送了,但是主机 2 还是可以发送数据到主机 1 的。那么就还有可能主机2会继续给主机1发送数据,所以挥手不能只有三次,需要多一次,主机2告诉主机1,我的数据也已经发送完毕了,这样才能结束。
-
什么是半连接?
半连接发生在TCP的三次握手中,如果主机A向主机B发起连接请求,主机B也按照正常情况下进行了响应,但是主机A不进行第三次握手,这个状态就是半连接状态。
-
四次挥手时最后为什么一定要等2MSL?
如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的 TCP 报文可能与新 TCP 连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的 TCP 连接的活跃报文全部失效,2MSL 时间可以满足这个需求。
-
TCP协议如何保证传输的可靠性的?
- 数据包校验
- 失序数据包重排
- 丢弃重复数据
- 应答机制,也就是收到数据的一方会向发送方回复一个确认消息(这个确认不是立即发送,通常推迟几分之一秒)。
- 超时重发,将数据发送出去之后会启动一个定时器,等待目的端的确认消息,如果在超时之前还没有收到确认消息,将会重新发送这个报文。
- 流量控制,TCP 连接的每一方都有固定大小的缓冲空间。TCP 的接收端只允许另一端发送接收端缓冲区所能接纳的数据,这可以防止较快主机致使较慢主机的缓冲区溢出,这就是流量控制。使用的流量控制协议为滑动窗口协议。
2 Socket编程
2.1 Socket 基础
Socket(套接字)是进程间通信的一种方式,与其他进程间通信方式不同,它可以实现不同主机间的进程通信。
它可以说是应用层和TCP/IP协议簇通信的中间软件抽象层,它是一组接口,它把复杂的TCP/IP协议簇隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
Python 提供了两个基本的 socket 模块。
- 一个是 Socket,它提供了标准的 BSD Sockets API。
- 另一个是 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。
我们在创建套接字的时候需要指定地址簇、套接字类型、协议编号(默认为0)
其中,地址簇可选项如下:
可选项 | 作用 |
---|---|
socket.AF_UNIX | 只能够用于单一的Unix系统进程间通信 |
socket.AF_INET | IPv4(默认) |
socket.AF_INET6 | IPv6 |
套接字类型可选项如下:
可选项 | 作用 |
---|---|
socket.SOCK_STREAM | 流式socket , for TCP (默认) |
socket.SOCK_DGRAM | 数据报式socket , for UDP |
socket.SOCK_RAW | 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。 |
socket.SOCK_RDM | 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。 |
socket.SOCK_SEQPACKET | 可靠的连续数据包服务 |
2.2 基于TCP套接字编程
2.2.1 编程思路
- 服务端:
- 创建套接字,绑定本地IP和port;
- 监听连接请求;
- 进入循环,不断接收客户端的连接请求;
- 然后接收传来的数据,并返回数据;
- 传输完毕,关闭套接字。
- 客户端:
- 创建套接字,连接服务端地址;
- 连接后发送数据,接收数据;
- 传输完毕后,关闭套接字。
2.2.2 常用方法
首先是创建套接字,一个基于TCP的套接字创建方式如下:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
常用方法如下:
s.bind(address)
:将套接字绑定到地址。s.listen(backlog)
:开始监听TCP的传入连接,backlog指在拒绝连接之前可以挂起的连接数量。s.accept()
:接收TCP连接,并返回(conn, address),其中conn是新的套接字对象,可以用来接收和发送数据,address是连接的客户端的地址。s.connect(address)
:连接到address处的套接字,格式为一个元组(ip, port),如果连接出错,返回socket error错误。s.recv(buffsize[, flag])
:接收TCP套接字数据。s.send(string[, flag])
:发送TCP套接字数据。s.sendall(string[, flag])
:完整发送TCP数据。s.close()
:关闭套接字。s.getpeername()
:返回连接套接字的远程地址。s.getsockname()
:返回套接字自己的地址。s.setsockopt(level, optname, value)
:设置给定套接字选项的值。s.getsockopt(level, optname[, buflen])
:返回套接字选项的值。s.settimeout(timeout)
:设置套接字操作的超时期。s.gettimeout()
:返回当前超时期的值,单位是秒。s.fileno()
:返回套接字的文件描述符。s.setblocking(flag)
:如果flag为0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。s.makefile()
:创建一个与该套接字相关联的文件。
2.2.3 客户端和服务端的简单实现
-
服务端
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) while True: print('等待客户端连接...') conn, client_obj = server.accept() print('客户端{}:{}连接成功!'.format(*client_obj)) while True: try: data = conn.recv(1024).decode('utf-8') if not data or data == 'quit': break data = data.upper() conn.send(data.encode('utf-8')) except Exception: break conn.close() print('客户端{}:{}断开连接!'.format(*client_obj)) server.close()
-
客户端
import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8080)) while True: data = input('请输入>>').strip() if not data: continue client.send(data.encode('utf-8')) if data == 'quit': break res_data = client.recv(1024) print(res_data.decode('utf-8')) client.close()
容易遇到的问题:
-
发送空的问题:
当客户端发送一个空时,客户端会卡主,一直在等待接收消息。但实际上是,这个空就没有发送出去,因为在send数据的时候会将数据发送到本机的缓存,然后系统再进行发送处理,那么我们发送一个空的时候,那么这个空实际上就是什么都没有给到缓存,也就是无法实际发送出去,那么服务端也就不会接收到任何数据,更不会发送回任何数据,这样客户端就会卡主,所以我们不能发送空,要对发送的数据进行一个限制。
-
突然关闭客户端
当我们突然关闭客户端时,如果我们不进行处理,在windows系统中,服务端会抛出异常,Unix系统中会出现一直收空的情况。因此我们在写代码的时候需要进行处理,在windows系统中需要捕获异常然后进行处理,在Unix系统中,正常情况下是不可能收到空的,所以只需要对收到的消息进行判断,如果有空,我们就断开连接即可。
-
“粘包”问题
“粘包”问题产生主要有两个原因,一个是由发送方引起的粘包问题,这是由TCP协议本身造成的,TCP为提高传输效率,发送方通常要收集到足够多的数据后才发送一次数据,若连续几次发送的数据都很少,通常会将这些数据合成一个包然后发送出去,这样接收方就接收到了粘包的数据;另一种是由接收方引起的粘包问题,这是由于接收方用户进程接收数据不及时导致的,因为接收方会把收到的数据放在系统缓冲区,用户进程从该缓冲区获取数据,若下一个包数据到达时前一个数据包尚未被用户进程取走,则下一个数据包的数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
产生“粘包”问题的本质是TCP传输中数据的无边界性导致的,那么我们就可以通过告知其边界来解决这个问题。也就是说在传输数据之前就先告诉对方要传输多大的数据。
2.3 基于UDP套接字编程
UDP实现起来相对比较简单。
这里用到的发送数据和接收数据的函数如下:
s.recvfrom(bufsize[.flag])
:接收UDP套接字数据。s.sendto(string[,flag],address)
:发送UDP套接字数据。
-
服务端
import socket udp_ser = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_ser.bind(('127.0.0.1', 8080)) while True: data, addr = udp_ser.recvfrom(1024) print("客户端{}:{}发来消息".format(*addr), data.decode('utf-8')) if data.decode('utf-8') == 'quit': break udp_ser.sendto(data.decode('utf-8').upper().encode('utf-8'), addr) udp_ser.close()
-
客户端
import socket udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) while True: data = input("请输入>>") udp_client.sendto(data.encode('utf-8'), ('127.0.0.1', 8080)) if data == 'quit': break recv, addr = udp_client.recvfrom(1024) print(recv.decode('utf-8')) udp_client.close()
3 socketserver模块
socketserver对socket进行了封装。
它有4个同步类:
- TCPServer
- UDPServer
- UnixStreamServer
- UnixDatagramServer
两个Mixin类,用来支持异步:
- ForkingMixin
- ThreadingMixin
组合得到:
- ForkingUDPServer(ForkingMixin, UDPServer)
- ForkingTCPServer(ForkingMixin, TCPServer)
- ThreadingUDPServer(ThreadingMixin, UDPServer)
- ThreadingTCPServer(ThreadingMixin, TCPServer)
其中带fork的需要操作系统支持,用于创建多进程,带thread的是创建多线程的。
创建服务器的步骤如下:
- 从BaseRequestHandler类派生出子类,并重写其handle()方法来创建请求处理程序类,此方法将处理传入的请求;
- 实例化一个服务器类,传入服务器的地址,以及请求处理类;
- 调用服务器的handle_request()或server_forever()方法;
- 调用server_close()关闭套接字。
对上面使用soket模块实现的进行改进,将其改成多线程模式,可以同时为多个客户端服务。
-
服务端
import socketserver class MyRequestHandler(socketserver.BaseRequestHandler): def handle(self) -> None: while True: try: data = self.request.recv(1024).decode('utf-8') if not data or data == 'quit': break data = data.upper() self.request.send(data.encode('utf-8')) except Exception: self.request.close() s = socketserver.ThreadingTCPServer(('127.0.0.1', 8888), MyRequestHandler) s.serve_forever()
客户端不需要进行任何改动。
该模块主要是为服务端准备的。