• 客户端与服务端


    客户端/服务端大致分为两套,一套是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
  • 相关阅读:
    android下socket编程问题:服务器关闭时,客户端发送请求的异常处理
    MySQL新建用户,授权,删除用户,修改密码
    jquery验证表单代码
    Incorrect key file for table '/tmp/#sql_46fd_0.MYI'; try to repair it
    初试百度地图API
    Android控件之GridView探究
    使用Intent调用内置应用程序
    消除SDK更新时的“https://dl-ssl.google.com refused”错误
    A folder failed to be renamed or moved--安装Android SDK的问题
    windows下搭建svn服务器
  • 原文地址:https://www.cnblogs.com/maxiaonong/p/9498351.html
Copyright © 2020-2023  润新知