客户端与服务端

客户端/服务端大致分为两套,一套是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)
View Code

下面是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'))
View Code

先大致说一下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()
View Code

如下是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()
View Code

简单点评,上面写得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'))
View Code
 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()
View Code

两种方法对应的服务端分别如下:

 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('数据已经真的发送出去')
View Code
 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)
View Code

关于之前说的不能并发问题,这里引入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()                                       # 这就是以前的大循环,也就是链接循环
View Code

对应的可并发加客户端认证的客户端代码是(客户端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'))
View Code
原文地址:https://www.cnblogs.com/maxiaonong/p/9498351.html