客户端/服务端大致分为两套,一套是TCP,一套是UDP。先看udp,tcp协议建立连接是要先发起链接的,而UDP没有链接,所以写的简单点。
下面是UDP的服务端:
1 from socket import * 2 3 ip_duan = ('127.0.0.1', 8000) 4 buff = 1024 5 6 udp_server = socket(AF_INET, SOCK_DGRAM) # SOCK_STREAM是流式的套接字,sock_dgram是数据报式 7 udp_server.bind(ip_duan) 8 # 因为没有链接,所以没有listen,当然也就没有accept,直接进入通信循环 9 10 while True: 11 data, addr = udp_server.recvfrom(buff) #recv是tcp,recvfrom是udp 12 # 返回的是一个元组,第一个是数据内容,第二个是客户端的IP+端口 13 print(data.decode('utf-8')) 14 udp_server.sendto(data.upper(),addr)
下面是UDP的客户端1与2(两个一样)
1 from socket import * 2 3 ip_duan = ('127.0.0.1', 8000) 4 buff = 1024 5 6 udp_client = socket(AF_INET, SOCK_DGRAM) # SOCK_STREAM是流式的套接字,sock_dgram是数据报式 7 # udp_client.bind(ip_duan) 8 # 因为没有链接,所以没有listen,当然也就没有accept,直接进入通信循环 9 10 while True: 11 msg = input('===>') 12 udp_client.sendto(msg.encode('utf-8'),ip_duan) # 没有链接,所以每次发的时候都要指定ip+duankou 13 print('客户端数据已经发送') 14 data, addr = udp_client.recvfrom(buff) 15 print(data.decode('utf-8'))
先大致说一下TCP与UDP区别:
UDP不用建立链接,所以服务器不用listen,accept,客户端发是sendto(发时代ip+ipot),收是recvfrom。
TCP不可以发空(空不是空格,是直接回车),所以发东西后要判断非空;但是udp从表面上看可以。
TCP服务端同一时刻只能服务一个客户端,第二个链接先挂起,等第一个客户端通讯(聊天)退出才到第二个通讯;UDP由于没有链接可以轻松实现并发。
TCP可能有粘包现象(无论TCP与UDP的发送还是接收,他们都是先到自己的缓存区,由于不知道收多少字节,可能会没收完,下次放一起),UDP永远不会有。
下面是说TCP,如下是TCP的服务端:
1 from socket import * 2 3 ip_duan = ('127.0.0.2', 8000) 4 back_log = 5 # 缓存池大小 5 buff = 1024 # 接收缓存大小字节 6 7 tcp_server = socket(AF_INET, SOCK_STREAM) 8 tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) #当出现address already in use 错误时,可以加这条避免 9 tcp_server.bind(ip_duan) 10 tcp_server.listen(back_log) 11 12 print('服务器正在执行===============》') 13 14 while True: # 循环可以接受多个链接,也就是说不只是为一个客户端服务 15 conn, address = tcp_server.accept() 16 print('双向连接',conn) 17 print('新的客户端链接',address) 18 19 while True: # 这个循环是可以多次通话 20 try: # 加try except是为了当一个客户端端口时,不至于因为没有conn而报错 21 msg = conn.recv(buff) 22 if not msg: 23 break # 解决死循环 24 print('服务器收到来自客户端的信息是: ',msg.decode('utf-8')) 25 conn.send(msg.upper()) 26 except Exception: 27 break 28 29 conn.close() 30 31 tcp_server.close()
如下是TCP的客户端1与2:
1 from socket import * 2 3 ip_duan = ('127.0.0.2', 8000) 4 buff = 1024 5 6 tcp_client = socket(AF_INET, SOCK_STREAM) 7 tcp_client.connect(ip_duan) 8 9 while True: 10 msg = input('==>') 11 if not msg: 12 continue 13 tcp_client.send(msg.encode('utf-8')) 14 print('客户端消息已经发送') 15 data = tcp_client.recv(buff) 16 print('客户端收到来自服务器的消息是:',data.decode('utf-8')) 17 18 tcp_client.close()
简单点评,上面写得TCP的c/s还有很多问题没有解决,比如最明显的是:1)不能并发,比如两个客户端,虽然客户端1,2都能连接,但是客户端1通讯时,客户端2只是链接挂起,它的通讯必须等1结束才行;2)TCP粘包问题;
关于TCP粘包问题:
粘包现象:(首先要知道,对于TCP来说,不一定说一个send对应一个recv,所以有粘包现象)
1)客户端连续发送,比如连续发送5个字节的数据,但是服务器定义的是每次从缓存区取1024字节,这样就会将客户端多次发送的多个包当做一个包接收
2)客户端发一个包,但是这个数据量有点大,比如一个包里的数据是1024个字节,但是服务器每次只收5个字节,这样服务器就会将一个包当做多个包来接收
粘包现象只存在于TCP协议,是因为TCP协议底层为了加快传输,用了Nagle算法(因为传输的时间消耗比电脑的运行大,所以当每次发送的数据小时,
会自动将几个连续发送的放在一起传输出去),UDP不存在粘包现象,虽然都是发向自己的缓存区,在自己的缓存区中接收,但是他们的发送方式不同,
tcp基于消息流,UDP是数据报,它发送的消息里不仅有消息还带IP加端口,这样相当于形成了一个包尾,这样接收消息时就有断开的依据。
TCP会粘包,但是不会丢数据,因为这次收不完下次收;UDP不会粘包,但是会丢数据。
下面是解决TCP粘包的两种方法(其实两种方法差不多):
1)LOW版本:客户端发送数据前,先发数据长度,但是数据不是紧跟数据长度一起发送的,因为这样会粘包,所以顺序是:客户端先发包头(数据长度),
服务器收到后,给一个回应,客户端收到回应后,开始发数据。然后服务器开始循环接收数据。
2)简洁版本:核心意思同上,import struct ,用其中的struct.pack,这样就可以固定数据长度这个信息所占的字节,这样客户端就没必要用回应隔开,
数据长度与数据粘包也没事,因为知道这个粘后的包前多少字节是长度信息。
1 from socket import * 2 3 # low版本 4 5 ip_port = ('127.0.0.1',8000) 6 back_log = 5 7 buff = 1024 8 9 tcp_server = socket(AF_INET,SOCK_STREAM) 10 tcp_server.bind(ip_port) 11 tcp_server.listen(back_log) 12 13 conn,add = tcp_server.accept() 14 length = int(conn.recv(buff).decode('utf-8')) # 接收长度 15 conn.send(b'ready') # 给一个回应 16 17 recv_size = 0 18 recv_msg = b'' 19 20 while recv_size < length: # 这段循环是因为,假如发送的数据量大于缓存区时 21 recv_msg += conn.recv(buff) 22 recv_size = len(recv_msg) 23 24 print('收到数据是:',recv_msg.decode('utf-8'))
1 from socket import * 2 import struct 3 # 简洁版本 4 5 ip_port = ('127.0.0.1', 8001) # 自己玩单机好像只可以用这个地址 6 back_log = 5 7 buff = 1024 8 9 tcp_server = socket(AF_INET, SOCK_STREAM) 10 # tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 当出现address already in use 错误时,可以加这条避免 11 tcp_server.bind(ip_port) 12 tcp_server.listen(back_log) 13 print('----------------') 14 15 while True: # 循环接收链接 16 conn, address = tcp_server.accept() 17 18 while True: # 与一个客户端的循环通话 19 try: 20 length_data = conn.recv(4) # 消息的长度信息虽然与消息粘包,但是将长度信息固定在前4个字节,取到的封装的字节流 21 length = struct.unpack('i', length_data)[0] # 将消息长度解包,解包后是一个元组,取第一个就是长度数据,int型 22 23 recv_size = 0 24 recv_msg = b'' 25 26 while recv_size < length: 27 recv_msg += conn.recv(buff) 28 recv_size = len(recv_msg) 29 30 print('收到的数据是:', recv_msg.decode('utf-8')) 31 except Exception: 32 break 33 34 conn.close() 35 36 tcp_server.close()
两种方法对应的服务端分别如下:
1 from socket import * 2 3 # low版本 4 5 ip_port = ('127.0.0.1',8000) 6 buff = 1024 7 8 tcp_client = socket(AF_INET,SOCK_STREAM) 9 tcp_client.connect(ip_port) 10 11 msg = input('===>') 12 length = len(msg) # 注意这里是int型 13 tcp_client.send(str(length).encode('utf-8')) # 发送数据长度 14 data_ready = tcp_client.recv(buff) 15 if data_ready.decode('utf-8') == 'ready': 16 tcp_client.send(msg.encode('utf-8')) 17 print('数据已经真的发送出去')
1 from socket import * 2 import struct 3 # 简洁版本 4 5 ip_port = ('127.0.0.1', 8001) 6 buff = 1024 7 8 tcp_client = socket(AF_INET, SOCK_STREAM) 9 tcp_client.connect(ip_port) 10 11 while True: 12 msg = input('=====>') 13 if not msg: 14 continue 15 length = len(msg) 16 length_data = struct.pack('i',length) # 将消息长度信息包装成4个字节的字节流形式 17 tcp_client.send(length_data) 18 tcp_client.send(msg.encode('utf-8')) # 这样发送两个发送是粘包在一起的,但是接收时已经考虑到这个问题,不怕 19 print('发出去的数据是:',msg)
关于之前说的不能并发问题,这里引入socketserver,用多线程解决并发问题,其客户端的代码如下:
另外以上是一些刚性问题,下面还有一个安全问题,比如如何防范客户端洪水攻击,如何阻止别的客户端链接(知道你的IP+端口):
因此为了安全应该加客户端认证,也就是在链接连接后,通讯前,进行客户端认证。怎么认证?
链接后,服务端发一个加盐的加密,服务端回复一个,看对不对。这样首先人家不知道怎么加密的,更不知道加的盐是什么。
1 import struct 2 import socketserver 3 import hmac,os 4 5 6 secret_key = b'wan yifei' 7 8 def conn_auth(conn): 9 ''' 10 认证客户端链接 11 :param conn: 12 :return: 13 ''' 14 15 print('开始认证客户端的合法性') 16 msg = os.urandom(32) # 先产生32位密文 17 conn.sendall(msg) # 发给客户端 18 h = hmac.new(secret_key,msg) # 加盐 19 digest = h.digest() 20 response = conn.recv(len(digest)) # 接收客户端的认证回应 21 return hmac.compare_digest(response,digest) # 比价认证结果 22 23 24 class Myserver(socketserver.BaseRequestHandler): 25 def handle(self): # 必须是handle 26 print('conn is : ',self.request) # 相当于之前建立链接后的conn 27 print('addr is : ',self.client_address) # addr 28 29 if not conn_auth(self.request): 30 print('该链接不合法,关闭') 31 self.request.close() 32 return 33 34 print('客户端合法') 35 while True: # 通讯循环 36 37 try: 38 # 收消息 39 length_data = self.request.recv(4) 40 length = struct.unpack('i',length_data)[0] 41 42 recv_size = 0 43 recv_msg = b'' 44 while recv_size < length: 45 recv_msg += self.request.recv(buff) 46 recv_size = len(recv_msg) 47 48 if not length_data: 49 break 50 print('收到客户端的消息是:',recv_msg.decode('utf-8')) 51 52 # 发消息 53 self.request.sendall(recv_msg.upper()) 54 except Exception as e: 55 print(e) 56 break 57 58 59 60 if __name__ == '__main__': 61 buff = 1024 62 ip_port = ('127.0.0.1', 8080) 63 s = socketserver.ThreadingTCPServer(ip_port, Myserver) # 这是多线程,意思就是来一个通讯就给它一个实例化(处理链接) 64 # 如果不是windows系统用ForkingTCPServer(多线程)代替ThreadingTCPServer也可以 65 s.serve_forever() # 这就是以前的大循环,也就是链接循环
对应的可并发加客户端认证的客户端代码是(客户端1,2都一样):
1 from socket import * 2 import struct 3 import hmac,os 4 5 ip_port = ('127.0.0.1', 8080) 6 buff = 1024 7 8 tcp_client = socket(AF_INET, SOCK_STREAM) 9 tcp_client.connect(ip_port) 10 11 12 secret_key = b'wan yifei' 13 14 def conn_auth(conn): 15 ''' 16 认证客户端链接 17 :param conn: 18 :return: 19 ''' 20 21 print('开始认证客户端的合法性') 22 msg = conn.recv(32) 23 h = hmac.new(secret_key,msg) # 加盐 24 digest = h.digest() 25 conn.sendall(digest) 26 27 conn_auth(tcp_client) 28 29 while True: 30 msg = input('=====>') 31 if not msg: 32 continue 33 length = len(msg) 34 length_data = struct.pack('i',length) # 将消息长度信息包装成4个字节的字节流形式 35 tcp_client.send(length_data) 36 tcp_client.send(msg.encode('utf-8')) # 这样发送两个发送是粘包在一起的,但是接收时已经考虑到这个问题,不怕 37 print('发出去的数据是:',msg) 38 data = tcp_client.recv(1024) 39 print('收到的数据是: ',data.decode('utf8'))