网络编程:tcp、udp、socket、struct、socketserver
一、TCP、UDP
一、ARP(Address Resolution Protocol)即地址解析协议,用于实现从 IP 地址到 MAC 地址的映射,即询问目标IP对应的MAC地址。 二、在网络通信中,主机和主机通信的数据包需要依据OSI模型从上到下进行数据封装,当数据封装完整后,再向外发出。所以在局域网的通信中,不仅需要源目IP地址的封装,也需要源目MAC的封装。 三、一般情况下,上层应用程序更多关心IP地址而不关心MAC地址,所以需要通过ARP协议来获知目的主机的MAC地址,完成数据封装。 ARP协议通过"一问一答"实现交互,但是"问"和"答"都有讲究,"问"是通过广播形式实现,"答"是通过单播形式。
传输层:TCP与UDP协议
TCP(传输控制协议)是一种可靠的通信协议,需要经过三次握手的环节,确立连接关系之后,才可以进行传输;终止连接(四次挥手);为确保正确地接收数据,TCP要求在目标计算机成功收到数据时发回一个确认(即ACK),如果在某个时限内未收到相应的 ACK,将重新传送数据包。
UDP(用户数据报协议)不一定提供可靠的数据传输,不需要建立连接关系,它只管传输。
TCP与UDP区别: 1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接; 2、TCP提供可靠的服务;也就是说,通过TCP连接传输的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,但不保证可靠交付; 3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等); 4、每一条TCP连接只能是点到点的;UDP支持一对一、一对多、多对一和多对多的交互通信; 5、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
二、socket
socket是一种操作系统提供的进程间通信机制;在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(socket API);应用程序可以通过套接字接口,来使用网络套接字,以进行数据交换。
基于TCP协议的socket
tcp是基于连接的,必须先启动服务端,再启动客户端去连接服务端。
server端
import socket sk = socket.socket() # 比喻买手机 sk.bind(("127.0.0.1", 8080)) # 把地址绑定到套接字;比喻绑定手机卡,接收的是元祖("ip", "port") sk.listen() # 监听链接;比喻等着他人给我打电话 conn, addr = sk.accept() # 接收客户端链接;比喻接受他人的电话,定义接收变量conn(链接)/addr(对方地址) msg = conn.recv(1024) # 接收客户端信息;比喻听他人说话,必须有参数(一般为1024的整数倍) print("client:", msg) # 打印客户端信息 conn.send(b"Hi!") # 向客户端发送信息 msg = conn.recv(1024).decode("utf-8") # 接收中文信息 print("client:", msg) conn.send(bytes("滚!", encoding="utf-8")) conn.close() # 关闭客户端套接字;比喻挂电话 sk.close() # 关闭服务器套接字(可选);比喻关手机
client端
import socket sk = socket.socket() # 比喻买手机 sk.connect(("127.0.0.1", 8080)) # 比喻播他人的电话 sk.send(b"Hello!") msg = sk.recv(1024) print("server:", msg) sk.send(bytes("星期日你有空吗?", encoding="utf-8")) msg = sk.recv(1024).decode("utf-8") print("server:", msg) sk.close()
如果遇到这种错误:
解决方法:导入一条socket配置,并在bind()前加入如下内容。
使用while循环使server与client持续对话
import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080)) sk.listen() conn, addr = sk.accept() while True: msg = conn.recv(1024).decode("utf-8") if msg == "88": break print(msg) info = input(">>>") if info == "88": conn.send(bytes(info, encoding="utf-8")) break else: conn.send(bytes(info, encoding="utf-8")) conn.close() sk.close()
import socket sk = socket.socket() sk.connect(("127.0.0.1", 8080)) while True: info = input(">>>") if info == "88": sk.send(bytes(info, encoding="utf-8")) break else: sk.send(bytes(info, encoding="utf-8")) msg = sk.recv(1024).decode("utf-8") if msg == "88": break print(msg) sk.close()
小练习:
- client 每隔5秒把时间戳发给server
- server 接收时间戳,将其转换成格式化时间
import time import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080)) sk.listen() conn, addr = sk.accept() while True: msg = conn.recv(1024).decode("utf-8") print(time.ctime(float(msg)))
import time import socket sk = socket.socket() sk.connect(("127.0.0.1", 8080)) while True: timestamp = time.time() sk.send(bytes(str(timestamp), encoding="utf-8")) time.sleep(5)
基于UDP协议的socket
server端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind(("127.0.0.1", 8080)) msg, addr = sk.recvfrom(1024) print("client:", msg) sk.sendto(b"Hi!", addr) sk.close()
client端
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.sendto(b"Hello!", ("127.0.0.1", 8080)) msg, addr = sk.recvfrom(1024) print("server:", msg) sk.close()
socket参数说明:
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
famliy:地址族;默认为AF_INET;还有AF_INET6、AF_UNIX、AF_CAN、AF_RDS;(AF_UNIX实际上是使用本地 socket 文件来通信)。
type:套接字类型;应为SOCK_STREAM(默认值,基于TCP)、SOCK_DGRAM(基于UDP)、SOCK_RAW或其他SOCK_常量之一。
proto:协议号;通常为零,可以省略;或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。
fileno:如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。这可能有助于使用socket.close()关闭一个独立的插座。
黏包
在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。所以UDP不会出现粘包问题。
TCP的长连接与短连接:
- 长连接:Client方与Server方先建立通讯连接,连接建立后不断开,然后再进行报文发送和接收。
- 短连接:Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点通讯,比如多个Client连接一个Server。
黏包出现原因:在流传输(TCP)中出现;UDP不会出现粘包,因为它有消息边界。
- 发送端需要等缓冲区满才发送出去,造成粘包;
- 接收方不及时接收缓冲区的包,造成多个包接收。
具体分析一下:
- 发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次性发送出去,这样接收方就收到了粘包数据。
- 接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据;若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
粘包情况:一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。
不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。
为什么基于TCP的通讯程序需要进行封包和拆包:
TCP是个"流"协议,所谓流,就是没有界限的一串数据,大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况。
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况)。
- A、先接收到data1,然后接收到data2;
- B、先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部;
- C、先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据;
- D、一次性接收到了data1和data2的全部数据。
对于A这种情况正是我们需要的,不再做讨论。对于B、C、D的情况即为"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包,为了拆包就必须在发送端进行封包。
对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。
为什么会出现B、C、D的情况:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法;简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。这是对Nagle算法一个简单的解释,C和D的情况就有可能是Nagle算法造成的。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据;当应用层由于某些原因不能及时的把这些数据取出来,就会造成接收端缓冲区中存放了几段数据,如情况B。
struct模块解决黏包
该模块可以把要发送的数据长度转换成固定长度的字节(bytes)。这样客户端每次接收消息之前,只要先接收这个固定长度字节的内容,看一看接下来要接收的信息大小,那么最终接收的数据只要达到这个值就停止接收,就能刚好不多不少的接收完整的数据了。
import struct ret = struct.pack("i", 1234) # 第一个参数为Format字符串,可用的Format如下图所示 print(ret) # b'\xd2\x04\x00\x00'
利用struct模块解决黏包的远程执行cmd命令
# server端 import socket import subprocess import struct sk = socket.socket() ip_port = ("127.0.0.1", 8888) sk.bind(ip_port) sk.listen() conn, addr = sk.accept() while True: cmd = conn.recv(1024).decode("utf-8") if cmd == "q": break ret = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = ret.stdout.read() std_err = ret.stderr.read() std_out_err_len = len(std_out) + len(std_err) st_pk = struct.pack("i", std_out_err_len) conn.send(st_pk) conn.send(std_out) conn.send(std_err) conn.close() sk.close() # client端 import socket import struct sk = socket.socket() ip_port = ("127.0.0.1", 8888) sk.connect(ip_port) while True: cmd = input("cmd:") if cmd.lower() == "q": sk.send(cmd.encode("utf-8")) break sk.send(cmd.encode("utf-8")) sk_pk = sk.recv(4) st_unpk = struct.unpack("i", sk_pk)[0] print(sk.recv(int(st_unpk)).decode("gbk")) sk.close()
利用struct自定制报头发送文件(FTP):
import json import socket import struct ip_port = ("127.0.0.1", 8888) buffer_size = 1024 sk = socket.socket() sk.bind(ip_port) sk.listen() conn, addr = sk.accept() pack_len = conn.recv(4) # 先收报头4个bytes,得到报头长度的字节格式 head_len = struct.unpack("i", pack_len)[0] # 提取报头的长度 bytes_head = conn.recv(head_len) # 按照报头长度,接收报头的bytes格式 json_head = bytes_head.decode("utf-8") head = json.loads(json_head) print(head) file_size = head["file_size"] with open(head["file_name"], "wb") as f: while file_size: # 根据报头的内容提取真实的数据 if file_size >= buffer_size: content = conn.recv(buffer_size) f.write(content) file_size -= buffer_size else: content = conn.recv(file_size) f.write(content) break conn.close() sk.close()
import os import json import socket import struct ip_port = ("127.0.0.1", 8888) buffer_size = 1024 sk = socket.socket() sk.connect(ip_port) # 为避免粘包,必须自定制报头 head = {"file_dir": r"F:\电子书", "file_name": r"正则指引.pdf", "file_size": None} file_path = os.path.join(head["file_dir"], head["file_name"]) file_size = os.path.getsize(file_path) head["file_size"] = file_size json_head = json.dumps(head) bytes_head = json_head.encode("utf-8") print(bytes_head) head_len = len(bytes_head) pack_len = struct.pack("i", head_len) sk.send(pack_len) # 先发报头的长度,4个bytes sk.send(bytes_head) # 再发报头的字节格式 with open(file_path, "rb") as f: while file_size: # 发送真实内容的字节格式 if file_size >= buffer_size: content = f.read(buffer_size) sk.send(content) file_size -= buffer_size else: content = f.read(file_size) sk.send(content) break sk.close()
socket的更多使用方法:
服务端套接字函数
s.bind() 绑定(主机,端口号)到套接字
s.listen() 开始TCP监听
s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来
客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据
s.sendall() 发送TCP数据
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字
面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间
面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件
官方文档对socket模块下的socket.send()和socket.sendall()解释如下: socket.send(string[, flags]) Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Returns the number of bytes sent. Applications are responsible for checking that all data has been sent; if only some of the data was transmitted, the application needs to attempt delivery of the remaining data. send()的返回值是发送的字节数量,这个数量值可能小于要发送的string的字节数,也就是说可能无法发送string中所有的数据。如果有错误则会抛出异常。 – socket.sendall(string[, flags]) Send data to the socket. The socket must be connected to a remote socket. The optional flags argument has the same meaning as for recv() above. Unlike send(), this method continues to send data from string until either all data has been sent or an error occurs. None is returned on success. On error, an exception is raised, and there is no way to determine how much data, if any, was successfully sent. 尝试发送string的所有数据,成功则返回None,失败则抛出异常。 故,下面两段代码是等价的: #sock.sendall('Hello world\n') #buffer = 'Hello world\n' #while buffer: # bytes = sock.send(buffer) # buffer = buffer[bytes:]
验证客户端链接的合法性:
import os import hmac import socket secret_key = b"key" # 密钥 sk = socket.socket() sk.bind(("127.0.0.1", 6666)) sk.listen() conn, addr = sk.accept() def check_client(conn): random_bytes = os.urandom(32) # 随机生成的32位bytes类型数据字符串 conn.send(random_bytes) obj = hmac.new(secret_key, random_bytes) ser_digest = obj.digest() client_digest = conn.recv(1024) if client_digest == ser_digest: print("合法的客户端") return True else: print("非法的客户端") return False flag = check_client(conn) while flag: data = input(">>>") if data == "88": conn.send(data.encode("utf-8")) break else: conn.send(data.encode("utf-8")) msg = conn.recv(1024).decode("utf-8") if msg == "88": break print("client:", msg) conn.close() sk.close()
import hmac import socket sk = socket.socket() sk.connect(("127.0.0.1", 6666)) secret_key = b"key" # 密钥 random_bytes = sk.recv(1024) # 用和server端相同的手法对这个字符串进行摘要 obj = hmac.new(secret_key, random_bytes) digest = obj.digest() # 拿到密文 sk.send(digest) msg = sk.recv(1024) if msg: print(msg.decode("utf-8")) while True: data = input(">>>") if data == "88": sk.send(data.encode("utf-8")) break else: sk.send(data.encode("utf-8")) msg = sk.recv(1024).decode("utf-8") if msg == "88": break print("server:", msg) sk.close()
三、socketserver
基于socketserver的多客户端远程连接服务端
import socketserver HOST, PORT = "127.0.0.1", 8888 class MyServer(socketserver.BaseRequestHandler): def handle(self): while True: self.msg = self.request.recv(1024).decode("utf-8") # self.request相当于conn if self.msg == "88": self.server.shutdown() break print(self.msg) inp = input(">>>") self.request.send(inp.encode("utf-8")) if __name__ == "__main__": # 创建一个server,将服务地址绑定到127.0.0.1:8888 server = socketserver.ThreadingTCPServer((HOST, PORT), MyServer) # 让server永远运行下去,除非强制停止程序 server.serve_forever()
import socket HOST, PORT = "127.0.0.1", 8888 # 创建一个socket连接, SOCK_STREAM代表使用TCP协议 with socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) as sk: sk.connect((HOST, PORT)) while True: inp = input(">>>") if inp == "88": sk.send(inp.encode("utf-8")) break sk.send(inp.encode("utf-8")) msg = sk.recv(1024).decode("utf-8") print(msg) sk.close()