网络编程
socket套接字
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】
TCP协议Socket
tcp是基于链接的,必须先启动服务端,然后再启动客户端去连接服务端。
server端
import socket s = socket.socket() s.bind(('127.0.0.1',8888)) # 把地址绑定到套接字 s.listen() # 监听链接 conn,addr = s.accept() # 接受客户端链接, conn是客户端连接过来而在服务器端为其生成的一个连接实例 print(addr) print(conn) data = conn.recv(1024) # 接收客户端信息 print(data) # 打印客户端信息 conn.send(b'hi') # 向客户端发送信息 conn.close() # 关闭客户端套接字 s.close() # 关闭服务器套接字(可选)
client端
import socket s = socket.socket() # 创建客户套接字 s.connect(('127.0.0.1',8888)) # 尝试连接服务器 s.send(b'hello!') res = s.recv(1024) # 对话(发送/接收) print(res) s.close() # 关闭客户套接字
UDP协议Socket
udp是无链接的,启动服务之后可以直接接受消息,不需要提前建立链接。
server
import socket udp_s = socket.socket(type=socket.SOCK_DGRAM) # 创建一个服务器的套接字 udp_s.bind(('127.0.0.1',9000)) # 绑定服务器套接字 msg,addr = udp_s.recvfrom(1024) print(msg) udp_s.sendto(b'hi',addr) # 对话(接收与发送) udp_s.close() # 关闭服务器套接字
client
import socket ip_port = ('127.0.0.1',9000) udp_c=socket.socket(type=socket.SOCK_DGRAM) udp_c.sendto(b'hello',ip_port) back_msg,addr=udp_c.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
socket参数
创建一个socket
socket.socket( family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
socket参数解析
参数 | 解析 |
family | 地址簇:socket.AF_INET : IPv4(默认) socket.AF_INET6: IPv6 socket.AF_UNIX: 只能够用于单一的Unix系统进程间通信,使用本地 socket 文件来通信 |
type | socket.SOCK_STREAM: 默认类型,数据流,TCP协议,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 socket.SOCK_DGRAM: 数据报式,UDP协议,无保障的面向消息的socket,多用于在网络上发广播信息。 |
proto | 协议号,通常为零,可以省略,与特定的地址家族相关的协议,如果是 0,则系统就会根据地址格式和套接类别,自动选择一个合适的协议;在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。 |
fileno | 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。与socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。这可能有助于使用socket.close()关闭一个独立的插座。 |
socket常用方法
方法 | 说明 |
s.bind(address) | 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址 |
s.listen(backlog) | 开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。 backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5。 这个值不能无限大,因为要在内核中维护连接队列。 |
s.setblocking(bool) | 是否阻塞,默认True,如果设置False,那么accept和recv时一旦无数据,则报错。 |
s.accept() | 接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。 接收TCP 客户的连接(阻塞式)等待连接的到来。 |
s.connect(address) | 连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
s.connect_ex(address) | 同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061。 |
s.close() | 关闭套接字。 |
s.recv(bufsize[,flag]) | 接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。 |
s.recvfrom(bufsize[.flag]) | 与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 |
s.send(string[,flag]) | 将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。 |
s.sendall(string[,flag]) | 将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。 内部通过递归调用send,将所有内容发送出去。 |
s.sendto(string[,flag],address) | 将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s ) |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。 |
s.getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr,port) |
s.fileno() | 套接字的文件描述符 |
黏包现象
黏包:同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。
TCP协议会出现黏包问题,UDP协议不会出现黏包。
server
from socket import * import subprocess ip_port=('127.0.0.1',8888) BUFSIZE=1024 s = socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind(ip_port) s.listen(5) while True: conn,addr = s.accept() print('客户端',addr) while True: cmd = conn.recv(BUFSIZE) if len(cmd) == 0:break res = subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stderr = res.stderr.read() stdout = res.stdout.read() conn.send(stderr) conn.send(stdout)
client
import socket
BUFSIZE=1024 ip_port=('127.0.0.1',8888) c = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = c.connect_ex(ip_port) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break c.send(msg.encode('utf-8')) act_res=c.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')
黏包产生
发送方的缓存机制
发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
# server from socket import * ip_port=('127.0.0.1',8080) s = socket(AF_INET,SOCK_STREAM) s.bind(ip_port) s.listen(5) conn,addr = s.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() # client import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
接收方的缓存机制
接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
# 服务端 from socket import * ip_port=('127.0.0.1',8080) s = socket(AF_INET,SOCK_STREAM) s.bind(ip_port) s.listen(5) conn,addr = s.accept() data1=conn.recv(2) # 一次没有收完整 data2=conn.recv(10) # 下次收的时候,会先取旧的数据,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close() # 客户端 import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) c = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = c.connect_ex(ip_port) c.send('hello egg'.encode('utf-8'))
注:
黏包现象只发生在tcp协议中:
- 从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
- 实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
黏包解决方案
在发送数据前,把将要发送的字节流总大小给接收端,接收端通过循环多次接受并判断是否接收完所有数据。
注意
接收端判断总接收字节大小时(recv_size < length),会出现接收到的总字节数大于发送端发送过来的总字节数。这是因为,len(一个汉字)=1,len('国'.encode())=3;如果客户端计算字节数不是统计encode()之后的数目,而客户端统计encode后的数目,那么客户端统计的总字节数大于发送端发送过来的总字节数。这也是为什么判断recv_size < length退出循环,而不是判断两者相等退出循环。
# server import socket,subprocess ip_port=('127.0.0.1',8080) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(ip_port) s.listen(5) while True: conn,addr=s.accept()while True: msg = conn.recv(1024) if not msg:break res=subprocess.Popen(msg.decode('utf-8'),shell=True,stdin=subprocess.PIPE,stderr=subprocess.PIPE,stdout=subprocess.PIPE) err = res.stderr.read() if err: ret = err else: ret = res.stdout.read() data_length = len(ret) conn.send(str(data_length).encode('utf-8')) data = conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close() # client import socket,time c = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = c.connect_ex(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if len(msg) == 0:continue if msg == 'quit':break c.send(msg.encode('utf-8')) length = int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) recv_size=0 data = b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))
端口占用问题
Windows
# 加入一条socket配置,重用ip和端口 import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 就是它,在bind前加 sk.bind(('127.0.0.1',8898)) # 把地址绑定到套接字 sk.listen() # 监听链接 conn,addr = sk.accept() # 接受客户端链接 ret = conn.recv(1024) # 接收客户端信息 print(ret) # 打印客户端信息 conn.send(b'hi') # 向客户端发送信息 conn.close() # 关闭客户端套接字 sk.close() # 关闭服务器套接字(可选)
Linux
Unix系统可以找到占用端口的进程,kill掉进程
socketserver
四种基本server类型
- class
socketserver.
TCPServer
(server_address, RequestHandlerClass, bind_and_activate=True) - class
socketserver.
UDPServer
(server_address, RequestHandlerClass, bind_and_activate=True) - class
socketserver.
UnixStreamServer
(server_address, RequestHandlerClass, bind_and_activate=True) - class
socketserver.
UnixDatagramServer
(server_address, RequestHandlerClass,bind_and_activate=True)
ThreadingTCPServer使用:
- 创建一个继承socketserver.BaseRequestHandler的类:class MyTCPHandler(socketserver.BaseRequestHandler)
- 重写handler方法: def handle(self):
- 实例化ThreadingTCPServer: server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
- 启动server: server.serve_forever()
server
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): self.data = self.request.recv(1024).strip() print("{} wrote:".format(self.client_address[0])) print(self.data) self.request.sendall(self.data.upper()) if __name__ == "__main__": HOST, PORT = "127.0.0.1", 9999 # 设置allow_reuse_address允许服务器重用地址 socketserver.TCPServer.allow_reuse_address = True # 创建一个server, 将服务地址绑定到127.0.0.1:9999 server = socketserver.TCPServer((HOST, PORT),Myserver) # 让server永远运行下去,除非强制停止程序 server.serve_forever()
client
import socket HOST, PORT = "127.0.0.1", 9999 data = "hello" # 创建一个socket链接,SOCK_STREAM代表使用TCP协议 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.connect((HOST, PORT)) # 链接到客户端 sock.sendall(bytes(data + " ", "utf-8")) # 向服务端发送数据 received = str(sock.recv(1024), "utf-8")# 从服务端接收数据 print("Sent: {}".format(data)) print("Received: {}".format(received))
实例

import os import json import hashlib import socketserver from conf import setting from core import sql class MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): while True: try: self.server_command = self.request.recv(1024).strip() print('receive command: ',self.server_command.decode() ) self.server_command = json.loads(self.server_command .decode()) if hasattr(self,self.server_command['command_key']): getattr(self,self.server_command['command_key'])() except ConnectionResetError as e: print('