网络编程之socket
1.socket概念
也叫做套接字。用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求,它是一个处于应用层和网路层之间的一个封装起来供人使用的接口
面向连接(TCP):通信之前一定要建立一条连接,这种通信方式也被成为”虚电路“或”流套接字“。面向连接的通信方式提供了顺序的、可靠地、不会重复的数据传输,而且也不会被加上数据边界。这意味着,每发送一份信息,可能会被拆分成多份,每份都会不多不少地正确到达目的地,然后重新按顺序拼装起来,传给正等待的应用程序。
要创建TCP套接字就得创建时指定套接字类型为SOCK_STREAM(默认不写)
无连接(UDP):无需建立连接就可以通讯。但此时,数据到达的顺序、可靠性及不重复性就无法保障了。数据报会保留数据边界,这就表示数据是整个发送的,不会像面向连接的协议先拆分成小块。它就相当于邮政服务一样,邮件和包裹不一定按照发送顺序达到,有的甚至可能根本到达不到。而且网络中的报文可能会重复发送。
要创建UDP套接字就得创建时指定套接字类型为SOCK_DGRAM
2.用法
first:实例化对象,构造函数:
obj = socket(family, type, proto, fileno, fileno)
family:地址簇,表示使用的协议,即TCP/IPv4的协议
type:表示TCP或者UDP
protocol :协议号,默认为0,一般不填
fileno:如果指定了fileno,其他参数将被忽略
second:进行绑定:
socket.bind(address)
address:IP地址与端口,这里是一个元组
third:开始监听:
socket.listen()
之前版本listen要添加参数n,参数n表示同一时间可以有n个链接等待与server端通信,一般不用
fourth:调用套接字函数等待连接
connection,address = socket.accept()
调用accept方法时,socket会进入'waiting'(或阻塞)状态。客户请求连接时,方法建立连接并返回服务器。accept方法返回一个含有俩个元素的元组,形如(connection,address)。第一个元素(connection)是新的socket对象,服务器通过它与客户通信;第二个元素(address)是客户的internet地址。
fifth:处理数据
服务器和客户通过send和recv方法传输数据。
服务器调用send,采用字符串形式向客户发送信息,send方法返回已发送的字符个数。
服务器使用recv方法从客户接受信息。
调用recv时,必须指定一个整数来控制本次调用所接受的最大数据量。recv方法在接受数据时会进入'blocket'状态,最后返回一个字符串,用它来表示收到的数据。如果发送的量超过recv所允许,数据会被截断。多余的数据将缓冲于接收端。以后调用recv时,多余的数据会从缓冲区删除。
sixth:关闭套接字
用法案例:
基于TCP协议的socket(TCP是基于链接的,必须先启动服务端,自启动客户端去链接客户端)
server端:
import socket soc = socket.socket() # 创建一个soc对象 soc.bind(('127.0.0.1',9789)) # 将地址绑定到套接字 soc.listen() # 监听端口 conn,addr = soc.accept() # 接收客户端口链接 re = conn.recv(1024) # 接收客户端信息 print(re.decode('utf-8')) # 接收客户端发送的信息,必须转码成unicode conn.send('海贼王'.encode('utf-8')) # 给客户端发送信息,必须是bytes类型 conn.close() soc.close()
client端:
import socket me = socket.socket() # 实例化对象 me.connect(('127.0.0.1',9789)) # 把地址绑定到套接字 me.send('one piece'.encode('utf-8')) # 给server发送消息 ret = me.recv(1024) # 设置固定接收字节,防止一次加载过多占内存 print(ret.decode('utf-8')) # 将server端发来的信息解码 me.close() # 关闭端口
易错点:这里客户端和服务端发送消息时,必须注意接收与发送顺序
计算机回环地址:
127.0.0.1,默认为本机地址,一般在自己电脑测试使用,它不用再过交换机查询
tcp协议适用范围:
适用于文件的上传和下载,以及发送重要文件等。每和一个客户端建立连接,都会在自己的操作系统上占用一个资源
它同一个时间段只能和一个客户端建立连接
基于UDP协议的socket(UDP是无链接的,先启动哪一端都不会报错)
用法案例:
server端:
import socket ser = socket.socket(type=socket.SOCK_DGRAM) ser.bind(('127.0.0.1', 8888)) mes,addr = ser.recvfrom(1024) print(mes.decode('utf-8')) ser.sendto('海贼王'.encode('utf-8'), addr) ser.close()
client端:
import socket client = socket.socket(type=socket.SOCK_DGRAM) addr = ('127.0.0.1', 8888) client.sendto('onepiece'.encode('utf-8'),addr) mess,addr = client.recvfrom(1024) print(mess.decode('utf-8')) client.close()
这样写虽然没有什么问题,但是基于需要来回编码以及解码,显得很不pythonic,为了解决这个问题,我们特别为内置的socket类编写一个子类,来解决编码转换的问题
创建类方法:
from socket import * class Mysocket(socket): def __init__(self,coding='utf-8'): self.coding = coding super().__init__(type=SOCK_DGRAM) def mysend(self,mess,addr): return self.sendto(mess.encode(self.coding),addr) def myrecv(self,num): mess,addr = self.recvfrom(num) return mess.decode(self.coding),addr
创建客户端和接收端
# server端 from my_test import Mysocket # 从文件路径中导入这个类 ser = Mysocket() ser.bind(('127.0.0.1', 8888)) mes,addr = ser.myrecv(1024) print(mes) ser.mysend('海贼王', addr) ser.close() # 用户端 from my_test import Mysocket client = Mysocket() addr = ('127.0.0.1', 8888) client.mysend('onepiece',addr) mess,addr = client.myrecv(1024) print(mess) client.close()
在UDP下基于服务端完成客户端的时间同步服务,比如机房中的所有机器每隔一段时间都会请求服务器,来获取一个标准时间
# server端
import time import socket sk = socket.socket(type=socket.SOCK_DGRAM) ip_port = ('127.0.0.1',9200) sk.bind(ip_port) while True: msg,addr = sk.recvfrom(1024) # 接收的是用户端格式化字符串字节码 sk.sendto(time.strftime(msg.decode('utf-8')).encode('utf-8'),addr) sk.close()
# client端
import socket import time tb = socket.socket(type=socket.SOCK_DGRAM) ip_port = ('127.0.0.1',9200) while True: tb.sendto('%Y/%m/%d %H:%M:%S'.encode('utf-8'),ip_port) mes,addr = tb.recvfrom(1024) print(mes.decode('utf-8')) time.sleep(1) tb.close()
3.关于TCP下的黏包问题
# server端 import socket ser = socket.socket() ip_port = ('127.0.0.1',8000) ser.bind(ip_port) ser.listen() myser,addr = ser.accept() myse = myser.recv(1024) print(myse.decode('utf-8')) myser.close() ser.close() # client端 import socket client = socket.socket() ip_port=('127.0.0.1',8000) client.connect(ip_port) client.send('第一次'.encode('utf-8')) client.send('第二次'.encode('utf-8')) client.send('第三次'.encode('utf-8')) client.send('第四次'.encode('utf-8')) client.send('第五次'.encode('utf-8')) client.close() server端接收结果:第一次第二次第三次第四次第五次
再看看这个:
# server端 import socket ser = socket.socket() ip_port = ('127.0.0.1',8000) ser.bind(ip_port) ser.listen() myser,addr = ser.accept() myser.send('一次一次一次'.encode('utf-8')) myser.send('二次'.encode('utf-8')) myser.send('三次'.encode('utf-8')) myser.send('四次'.encode('utf-8')) myser.send('五次'.encode('utf-8')) myser.close() ser.close() # client端 import socket client = socket.socket() ip_port=('127.0.0.1',8000) client.connect(ip_port) s = client.recv(9) # 一次没有完整接收 print(s.decode('utf-8')) s1 = client.recv(9) # 这次接收会继续接收上次剩下的数据流 print(s1.decode('utf-8')) s2 = client.recv(9) print(s2.decode('utf-8')) s3 = client.recv(9) print(s3.decode('utf-8')) s4 = client.recv(9) print(s4.decode('utf-8')) s5 = client.recv(3) print(s5.decode('utf-8')) client.close() # client接收: 一次一 次一次 二次三 次四次 五次
为什么client端的消息在server端可以一次性全部接收呢?第二个例子中server端的信息需要等待缓冲区满才发送过去呢?
TCP协议是面向流(stream)的协议,发送端为了将多个发往接收端的包更有效的发送给对方,使用了(Nagle算法)优化方法,将多次间隔较小且数据量小的数据合并成一个大数据块,然后进行。这样接收端就难以分辨出来了
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
tcp是基于数据流的,所以收发消息不能为空,这就需要在两个端口进行空消息处理机制,防止程序卡住
udp是基于数据报的,消息可为空(因为它存在消息保护边界)
UDP是面向消息的协议,每个UDP段都是一个消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
特点:
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
4.解决黏包的办法(导用struct模块)
黏包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
这里利用struct模块可以把一个类型(如数字),转换成固定长度的bytes类型(四个字节)
import struct s = struct.pack('i',12346587) print(s) # b'\xdbd\xbc\x00' a = struct.unpack('i',b'\xdbd\xbc\x00') print(a) # (12346587,)
用法:
server端
import os import json import struct import socket ser = socket.socket() ip_port = ('127.0.0.1',8000) ser.bind(ip_port) ser.listen() conn,addr = ser.accept() filename = '塑料王国.mp4' filesize = os.path.getsize('E:\电影\塑料王国.mp4') dic = {'filename':filename,'filesize':filesize} str_dic = json.dumps(dic) len_dic = struct.pack('i',len(str_dic)) conn.send(len_dic) # 这里必须是先发送字符串长度 conn.send(str_dic.encode('utf-8')) with open('E:\电影\塑料王国.mp4','rb') as f: while True: block = f.read(2048) conn.send(block) if not block: break ser.close() conn.close()
client端
import socket import json import struct cli = socket.socket() ip_port = ('127.0.0.1',8000) cli.connect(ip_port) sizedic = cli.recv(4) # 这里固定接收4个字节,和struct用法一致 sizedic = struct.unpack('i',sizedic)[0] str_size = cli.recv(sizedic).decode('utf-8') strdid = json.loads(str_size) print(strdid) with open('电影.mkv','wb') as f: while True: conn = cli.recv(2048) # 两边可以不用一样,这里接收任意大小 f.write(conn) if not conn: break cli.close()
5.验证客户端合法性
这里首先使用了os模块中的os.urandom(32)方法生成了一个固定长度且可变的bytes字节码
另MAC(Message Authentication Code,消息认证码算法)是含有密钥的散列函数算法,兼容了MD和SHA算法的特性,并在此基础上加入了密钥。
server端
import os import socket import hmac serect_key = '这是一个秘钥'.encode('utf-8') sk=socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() while True: try: conn,addr = sk.accept() rand = os.urandom(32) # 生成32位随机bytes字节码 conn.send(rand) obj = hmac.new(key=serect_key,msg=rand) ret = obj.hexdigest() # 得到一个密文 msg = conn.recv(1024).decode('utf-8') if msg == ret:print('合法用户') else:conn.close() finally: sk.close() break
client端
import socket import hmac serect_key = '这是一个秘钥'.encode('utf-8') client = socket.socket() client.connect(('127.0.0.1',9000)) urandom = client.recv(1024) # 客户端接收32位随机bytes字节码 hmac_obj = hmac.new(key=serect_key,msg=urandom) client.send(hmac_obj.hexdigest().encode('utf-8')) client.close()
6.socketserver的使用(起到了并发编程的作用)
通过该模块实现了多个client端可以同一时间向server端发送消息(它的内部其实通过并发编程实现的)
server端-----固定格式
import socketserver class Myserver(socketserver.BaseRequestHandler): def handle(self): # 创建一个方法,这里规定方法名必须叫做handle self.request.send('server端消息'.encode('utf-8')) msg = self.request.recv(1024).decode('utf-8') print(msg) # 这里的self.request就是conn,相当于拿到了链接 if __name__ == '__main__': socketserver.TCPServer.allow_reuse_address = True # 这话设置为True,防止端口报错 server = socketserver.ThreadingTCPServer(('127.0.0.1',8000),Myserver) server.serve_forever()
client端
import socket cli = socket.socket() cli.connect(('127.0.0.1',8000)) ret = cli.recv(1024).decode('utf-8') print(ret) inp = input('>>>').encode('utf-8') cli.send(inp) cli.close()
7.websocket的概念及使用