Socket层
Socket有一个缓冲区,缓冲区是一个流,先进先出,发送和取出的可自定义大小的,如果取出的数据未取完缓冲区,则可能存在数据怠慢。造成粘包的问题
黏包问题:文件大小和文件内容,一起在缓冲区发送给服务端,就会产生粘包的现象
Socket发送两条连续数据时,可能最终会拼接成一条进行发送
解决方法一:
两条数据间进行延时发送,如【tiem.sleep(0.5) #延时0.5s】
解决方法二:
每次发送后等待对方确认接收信息数据,发送一条后就立即接收等待
解决方法三:
设定接收数据大小,发送端每次发送需要发送的数据的数据大小,接收端通过设置【recv(xx)】只接收确定的大小
(在client发送文件大小后面加一个recv,直到server端回复,才开始发送文件)
Socket基本使用:
简单的服务器:
import socket sk = socket.socket() 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() #关闭服务器套接字(可选)
简单的客户端:
import socket sk = socket.socket() # 创建客户套接字 sk.connect(('127.0.0.1',8898)) # 尝试连接服务器 sk.send(b'hello!') ret = sk.recv(1024) # 对话(发送/接收) print(ret) sk.close() # 关闭客户套接字
Soket进阶:socket上传文件
- 先发送需要发送的文件的大小
- 然后用for循环一直发送文件
- 一直到接受的字节大小等于发送的字节大小
服务器进阶:实现客户循环连接及数据循环收发
import socket sk = socket.socket() sk.bind(('127.0.0.1', 9999,)) sk.listen(5) while True: conn, address = sk.accept() conn.sendall(bytes('欢迎上传文件', encoding='utf-8')) file_size = str(conn.recv(1024), encoding='utf-8') conn.sendall(bytes('服务端:已经接收到文件大小,开始接收文件。。。', encoding='utf-8')) print(file_size) totle_size = int(file_size) has_recv = 0 f = open('1.pdf','wb') while True: if totle_size == has_recv: break data = conn.recv(1024) f.write(data) has_recv += len(data) f.close()
客户端进阶:
import os import socket obj = socket.socket() obj.connect(('127.0.0.1',9999)) ret_bytes = obj.recv(1024) ret_str = str(ret_bytes,encoding='utf-8') print(ret_str) size = os.stat('《疯狂Python讲义》.pdf').st_size obj.sendall(bytes(str(size),encoding='utf-8')) # 收到服务器确认信息,才开始发送信息 print(str(obj.recv(1024),encoding='utf-8')) with open('《疯狂Python讲义》.pdf','rb') as f: for line in f: obj.sendall(line)
IO多路复用:
可以监听多个文件描述符(文件句柄)(Socket对象),一旦文件句柄出现变化,即可感知
send和sendall的区别
- send不一定一次发送完了,返回发送的字节数
- sendall里面是一个for循环,一直调用send
sever端:
import socket sk1 = socket.socket() sk1.bind(('127.0.0.1', 8001,)) sk1.listen() sk2 = socket.socket() sk2.bind(('127.0.0.1', 8002,)) sk2.listen() sk3 = socket.socket() sk3.bind(('127.0.0.1', 8003,)) sk3.listen() inputs = [sk1, sk2, sk3] import select while True: # select自动监听sk1-sk3三个对象,一旦某个句柄发生变化 # 如果有人链接,就会发生变化 # 最后一个参数每次循环,最多等待一秒 # r_list表示发生变化链接,e_list表示发生错误的链接,w_list有什么值,存储什么值 r_list, w_list, e_list = select.select(inputs, [],inputs, 1) # print(r_list, w_list, e_list) # 链接上就发送一个hello for sk in r_list: conn,address = sk.accept() conn.sendall(bytes(str('hello world!'),encoding='utf-8')) conn.close() for sk in e_list: inputs.remove(sk)
client端:
import socket obj = socket.socket() obj.connect(('127.0.0.1',8001)) content = str(obj.recv(1024),encoding='utf-8') print(content) obj.close()
IO多路复用(改进):一个sk实现IO多路复用
sever端:
import socket # 本质还是一个一个处理 sk1 = socket.socket() sk1.bind(('127.0.0.1', 8001,)) sk1.listen() inputs = [sk1] import select while True: # select自动监听sk1-sk3三个对象,一旦某个句柄发生变化 # 如果有人链接,就会发生变化 # 最后一个参数每次循环,最多等待一秒 # r_list表示发生变化链接,e_list表示发生错误的链接,w_list有什么值,存储什么值 r_list, w_list, e_list = select.select(inputs, [], inputs, 1) # print(r_list, w_list, e_list) # 链接上就发送一个hello print("正在监听的socket对象%d,发生变化的%s" % (len(inputs), r_list)) for sk in r_list: # 有新用户链接 if sk == sk1: conn, address = sk.accept() inputs.append(conn) else: try: # 有老用户来链接 data = sk.recv(1024) # 表示确实客户端确实发送数据了 data_str = str(data, encoding='utf-8') sk.sendall(bytes(data_str, encoding='utf-8')) # 表示客户端发送为空,客户端终止的时候 except Exception as ex: inputs.remove(sk) for sk in e_list: inputs.remove(sk)
client端:
import socket obj = socket.socket() obj.connect(('127.0.0.1',8001)) while True: inp = input(">>>") obj.sendall(bytes(inp,encoding='utf-8')) ret = str(obj.recv(1024),encoding='utf-8') print(ret) obj.close()
多路复用的读写分离:scrapy源码里就是这么写的
sever端:
import socket # 本质还是一个一个处理 sk1 = socket.socket() sk1.bind(('127.0.0.1', 8001,)) sk1.listen() inputs = [sk1, ] outputs = [] message_dict = {} import select while True: # select自动监听sk1-sk3三个对象,一旦某个句柄发生变化 # 如果有人链接,就会发生变化 # 最后一个参数每次循环,最多等待一秒 # r_list表示发生变化链接,e_list表示发生错误的链接,w_list有什么值,存储什么值 r_list, w_list, e_list = select.select(inputs, outputs, inputs, 1) # print(r_list, w_list, e_list) # 链接上就发送一个hello print("正在监听的socket对象%d,发生变化的%s" % (len(inputs), r_list)) # 通过r_list和w_list实现读写分离 # r_list只用来读取信息 for sk in r_list: # 有新用户链接 if sk == sk1: conn, address = sk.accept() inputs.append(conn) # 保存用户{'小周':[]} message_dict[conn] = [] else: try: # 有老用户来链接 data = sk.recv(1024) # 表示确实客户端确实发送数据了 data_str = str(data, encoding='utf-8') # 保存收到的消息 message_dict[sk].append(data_str) # sk.sendall(bytes(data_str, encoding='utf-8')) outputs.append(sk) # 表示客户端发送为空,客户端终止的时候 except Exception as ex: inputs.remove(sk) # w_list保存谁给我发送过消息, # w_list只用来回复信息 for conn in w_list: # 拿到接受的消息 recv_str = message_dict[conn][0] # 每一次取出数据后,在删除,等待下一次信息 del message_dict[conn][0] conn.sendall(bytes(recv_str+'好', encoding='utf-8')) outputs.remove(conn) for sk in e_list: inputs.remove(sk)
socket三阶段
-
socket,服务端只能处理一个请求
-
select + socket,伪并发
-
r_list:既读又写
-
r_list,w_list:读写分离
-
socket server
select + socket + 多线程 = 真并发
每次客户端链接,都创建一个线程去处理 = 同时处理所有请求,并发实现
类图关系:
源码流程
# -*- coding:utf-8 -*- import socketserver class MyServer(socketserver.BaseRequestHandler): def handle(self): # print self.request,self.client_address,self.server conn = self.request conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.') Flag = True while Flag: data = conn.recv(1024) if data == 'exit': Flag = False elif data == '0': conn.sendall('通过可能会被录音.balabala一大推') else: conn.sendall('请重新输入.') if __name__ == '__main__': # socket + select + 多线程 # 创建IP,端口,类名 # ThreadingTCPServer.init() => TCPServer.init() => BaseServer.init() # server对象 # self.server_address ('127.0.0.1',8009) # self.RequestHandlerClass MyServer # self.socket socket.socket() # self.server_bind() self.socket.bind() # self.server_activate() self.socket.listen() server = socketserver.ThreadingTCPServer(('127.0.0.1',8009),MyServer) # server对象的serve_forever方法 # 同样,在BaseServer中找到此方法,使用方法不一样,但是可以对应 # while not self.__shutdown_request while Ture # ready = selector.select(poll_interval) # _ServerSelector()--->selectors.SelectSelector--->select() # r, w, _ = self._select(self._readers, self._writers, [], timeout) # self._handle_request_noblock() # request, client_address = self.get_request() conn, address = sk.accept() # self.process_request t = threading.Thread() # self.finish_request()=> self.RequestHandlerClass(request, client_address, self) MyServer() # self.request = request # self.client_address = client_address # self.server = server # self.setup() # try: # self.handle() ----》 MyServer.handle() 最终目的达到 server.serve_forever()
服务端:
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()
客户端:
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))
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() 创建一个与该套接字相关的文件