Socket
socket概念
-
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
-
在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
套接字(socket)的使用
TCP协议中的使用
- .socket() tcp协议的server , socket(type=socket.SOCK_DGRAM)
- .bind 绑定一个id和端口
- .listen 监听,代表socket服务的开启
- .accept 等待,有客户端来访问和客户端建立连接
- .send 直接通过连接发送消息,不需要写地址
- .recv 只接受消息
- .connect 客户端/tcp协议的方法,和server建立连接
- .close 关闭服务/连接
sever端
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() #关闭服务器套接字(可选)
client端
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
UDP协议中的使用
- .socket(type=socket.SOCK_DGRAM) UDP协议的server
- .sendto 需要写一个对方的地址
- .recvfrom 接受消息和地址
- .close 关闭服务/连接
server端
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM) #创建一个服务器的套接字
udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr) # 对话(接收与发送)
udp_sk.close() # 关闭服务器套接字
client端
import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)
TCP、UDP协议多人通信
TCP协议的多人多次通信
- 和一个人通信说多次话
- 和一个人聊完再和其他人聊
UDP协议的多人多次通信
- 和一个人通信说多次话
- 和一个人聊完再和其他人聊
程序的几种阻塞方式
- input() 阻塞,直到用户输入enter键
- accept() 阻塞,由客户端和我建立完连接之后
- recv() 阻塞,知道收到对方发过来的消息之后
- recvfrom() 阻塞,知道收到对方发过来的消息之后
- connect() 阻塞,知道server端结束了对一个client的服务,开始和当前client建立连接的时候
socket参数的详解
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)
创建socket对象的参数说明
- family 地址系列应为AF_INET(默认值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。(AF_UNIX 域实际上是使用本地 socket 文件来通信)
- type 套接字类型应为SOCK_STREAM(默认值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。SOCK_STREAM 是基于TCP的,有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料传送。 SOCK_DGRAM 是基于UDP的,无保障的面向消息的socket,多用于在网络上发广播信息。
- proto 协议号通常为零,可以省略,或者在地址族为AF_CAN的情况下,协议应为CAN_RAW或CAN_BCM之一。fileno 如果指定了fileno,则其他参数将被忽略,导致带有指定文件描述符的套接字返回。与
- socket.fromfd()不同,fileno将返回相同的套接字,而不是重复的。这可能有助于使用socket.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() 创建一个与该套接字相关的文件
粘包现象
出现原因
-
只出现在tcp协议中,多条消息之间没有边界,并且有优化算法
-
发送端:两条消息发送间隔短,数据小,由于优化机制就合并在一起了
-
接受端:接受消息不及时,并且接收方缓存端堆在一起导致的粘包
本质
- tcp协议的传输是流式传输,数据与数据之间没有边界
解决
方法一(自定义协议)
- 通过自定义协议来解决问题,先发送固定长度的字节,通过接收知道数据的长度,按照数据长度,设置边界,不会黏在一起
Sever端
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, addr = sk.accept()
str_1 = conn.recv(4)
print(str_1)
length = int(str_1.decode('utf-8'))
print(length)
msg1 = conn.recv(length)
msg2 = conn.recv(1024)
print(msg1.decode('utf-8'))
print(msg2.decode('utf-8'))
conn.close()
sk.close()
Client端
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
msg1 = input(">>> ")
msg2 = input(">>> ")
length = len(msg1.encode('utf-8'))
print(length)
str_1 = str(length).zfill(4)
print(str_1)
sk.send(str_1.encode('utf-8'))
sk.send(msg1.encode('utf-8'))
sk.send(msg2.encode('utf-8'))
sk.close()
方法二(通过struct模块解决)
struct模块
方法
- struct.pack()方法:该模块可以把一个类型,如数字,转成固定长度的bytes
len1 = struct.pack('i', 1)
print(len1, len(len1)) # b'\x01\x00\x00\x00' 4
len2 = struct.pack('i', 123)
print(len2, len(len2)) # b'{\x00\x00\x00' 4
len3 = struct.pack('i', 12345)
print(len3, len(len3)) # b'90\x00\x00' 4
struct.pack('i',1111111111111) # struct.error: argument out of range
# 抛出异常
# struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围
- struct.unpack()方法:将固定长度的bytes转换回一个类型,如数字
int1 = struct.unpack('i', len1)
print(int1, int1[0]) # (1,) 1
int2 = struct.unpack('i', len2)
print(int2, int2[0]) # (123,) 123
int3 = struct.unpack('i', len3)
print(int3, int3[0]) # (12345,) 12345
验证客户端的合法性
方法一:hashlib验证
Server端
KEY = '123456'
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, attr = sk.accept()
os_ran = os.urandom(32)
conn.send(os_ran)
sha = hashlib.sha1(KEY.encode('utf-8'))
sha.update(os_ran)
client_sha = conn.recv(1024)
print(client_sha, '---', sha.hexdigest()) # 352d3b92a1dc5b96e79a9b032dafd2eb5188f8bc --- 352d3b92a1dc5b96e79a9b032dafd2eb5188f8bc
Client端
KEY = '123456'
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
os_ran = sk.recv(32)
sha = hashlib.sha1(KEY.encode('utf-8'))
sha.update(os_ran)
ret = sha.hexdigest()
sk.send(ret.encode('utf-8'))
方法二:hmac模块验证(简易版)
Server端
KEY = '123456'
sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()
conn, attr = sk.accept()
os_ran = os.urandom(32)
conn.send(os_ran)
hm = hmac.new(KEY.encode('utf-8'), os_ran)
client_hm = conn.recv(1024)
print(client_hm, hm.digest()) # b'\x9e !\x82\x9c\xbb\xf0\x06\xf22\xe7\x86c$\xaa\x89' b'\x9e!\x82\x9c\xbb\xf0\x06\xf22\xe7\x86c$\xaa\x89'
Client端
KEY = '123456'
sk = socket.socket()
sk.connect(('127.0.0.1', 9000))
os_ran = sk.recv(32)python
hm = hmac.new(KEY.encode('utf-8'), os_ran)
sk.send(hm.digest())
socketsever模块
- socketsever是基于socket(底层模块)模块完成的
作用
- 处理并发的客户端请求
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.uppythonper())
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 + "\n", "utf-8")) # 向服务端发送数据
received = str(sock.recv(1024), "utf-8")# 从服务端接收数据
print("Sent: {}".format(data))
print("Received: {}".format(received))