第八章 网络编程

  • 两个运行中的程序如何传递信息?
    通过文件
  • 两台机器上的两个运行程序如何通信?
    通过网络

8.1 网络应用开发架构

8.1.1C/S结构

全称 client / server 客户端 / 服务端

[C/S架构是第一种比较早的软件架构,主要用于局域网内。也叫 客户机/服务器模式。C/S架构软件有一个特点,就是如果用户要使用的话,需要下载一个客户端,安装后就可以使用。比如QQ,OFFICE软件等。

[

域架构级别_运维

  • 它可以分为客户机和服务器两层
  • 第一层: 在客户机系统上结合了界面显示与业务逻辑(即用户表示层)
  • 第二层: 通过网络结合了数据库服务器(数据库层)。

  这里需要补充的是,客户端不仅仅是一些简单的操作,它也是会处理一些运算,业务逻辑的处理等。也就是说,客户端也做着一些本该由服务器来做的一些事情.

C/S架构的优点:

  • 1 C/S架构的界面和操作可以很丰富。(客户端操作界面可以随意排列,满足客户的需要)
  • 2 安全性能可以很容易保证。(因为只有两层的传输,而不是中间有很多层。
  • 3 由于只有一层交互,因此响应速度较快。(直接相连,中间没有什么阻隔或岔路,比如QQ,每天那么多人在线,也不觉得慢)

C/S架构的缺点:

可以将QQ作为类比:

  • 1 适用面窄,通常用于局域网中。
  • 2 用户群固定。由于程序需要安装才可使用,因此不适合面向一些不可知的用户。
  • 3 维护成本高,发生一次升级,则所有客户端的程序都需要改变。

8.1.2B/S结构

全称为Browser/Server,即浏览器/服务器结构。

Browser指的是Web浏览器,极少数事务逻辑在前端实现,但主要事务逻辑在服务器端实现.B/S架构的系统无须特别安装,只有Web浏览器即可。其实就是我们前端现在做的一些事情,大部分的逻辑交给后台来实现,我们前端大部分是做一些数据渲染,请求等比较少的逻辑。


B/S架构的分层:

与C/S架构只有两层不同的是,B/S架构有三层,分别为:

  • 第一层表现层:主要完成用户和后台的交互及最终查询结果的输出功能。
  • 第二层逻辑层:主要是利用服务器完成客户端的应用逻辑功能。
  • 第三层数据层:主要是接受客户端请求后独立进行各种运算。

B/S架构的优点:

  • 1、客户端无需安装,有Web浏览器即可。
  • 2、BS架构可以直接放在广域网上,通过一定的权限控制实现多客户访问的目的,交互性较强。
  • 3、BS架构无需升级多个客户端,升级服务器即可。可以随时更新版本,而无需用户重新下载啊什么的。

B/S架构的缺点:

  • 1.在跨浏览器上,BS架构不尽如人意。
  • 2、表现要达到CS程序的程度需要花费不少精力。
  • 3、在速度和安全性上需要花费巨大的设计成本,这是BS架构的最大问题。
  • 4、客户端服务器端的交互是请求-响应模式,通常需要刷新页面,这并不是客户乐意看到的。(在Ajax风行后此问题得到了一定程度的缓解)

注 :B/S架构和C/S架构关系?**

B/S是特殊的C/S架构

 

8.2网络基础

  • 通信协议的构成
  • 一是语义部分,用于决定双方对话的类型
  • 二是语法部分,用于决定双方对话的格式
  • 三是变换规则,用于决定通信双方的应答关系

8.2.1 基本概念

几个重要的物理设备

  • 网卡 :是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件。由于其拥有MAC地址,因此属于OSI模型的第1层。它使得用户可以通过电缆或无线相互连接。
  • 交换机 :完成局域网内多台机器的通信 ,只认识mac地址
  • mac地址 :每一块网卡上都有一个全球唯一的mac地址 , 用于识别计算机
  • 单播,广播, 组播
  • arp协议 : 又称地址解析协议,通过ip获取它的mac地址,是由交换机完成,请求发出使用广播,请求返回使用单播
  • 协议 : 两台物理设备之间对于要发送的内容,长度,顺序的一些约定
  • 路由器 : 完成局域网与局域网之间的通信 ,可识别ip地址
  • 网关 ip :访问局域网外部设备的一个出口ip,访问局域网之外的区域都需要经过路由器和网关
  • 网段 指的是一个地址段 x.x.x.0 x.x.0.0 x.0.0.0
  • 子网掩码的作用 : 判断两台机器是否在同一个网段内的
255.255.255.0 子网掩码
11111111.11111111.11111111.00000000

192.168.12.87
11000000.10101000.00001100.01010111
11111111.11111111.11111111.00000000
11000000.10101000.00001100.00000000   192.168.12.0

192.168.12.7
11000000.10101000.00001100.00000111
11111111.11111111.11111111.00000000
11000000.10101000.00001100.00000000  192.168.12.0

8.2.2一个程序如何在网络找能够找到另一个程序

ip地址精确到具体的一台电脑,而端口精确到具体的程序。

首先,程序必须要启动,其次,必须有这台机器的地址,我们都知道我们人的地址大概就是国家\省\市\区\街道\楼\门牌号这样字。那么每一台联网的机器在网络上也有自己的地址,它的地址是怎么表示的呢?

8.2.2.1什么是ip地址?

IP地址是指互联网协议地址(英语:Internet Protocol Address,又译为网际协议地址),是IP Address的缩写。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),实际上是32位二进制数(01100100.00000100.00000101.00000110)。

8.2.2.2公网ip和内网ip

划分内网和外网的重要依据主要是判断它是不是与广阔的外界相连。 此时我们可以说,局域网就是内网,internet就是外网,当然,内网和外网不是绝对的概念,而是相对的

  • 为什么你的外地朋友的电脑我们访问不了?
    每一个ip地址要想被所有人访问到,那么这个ip地址必须是你申请的
  • 内网ip
    10 0.0.0 10.255.255.255172.16.0.0 172 31.255 255192.168.0.0 92.168.255.255


8.2.2.3 ipv4和ipv6协议
  • ipv4和ipv6协议
  • ipv4 :4位的点分十进制 32位2进制表示. 范围 : 0.0.0.0 - 255.255.255.255
  • ipv6 :6位的冒分十六进制 128位2进制表示.范围 : 0:0:0:0:0:0-FFFF:FFFF:FFFF:FFFF:FFFF:FFFF
8.2.2.4 port 端口
  • port能够在网络上定位一台机器上的一个服务
  • 范围 : 0-65535 (最好写8000以上)
  • ip + port 确认一台机器上的一个应用 ,如果占用了80端口,那么其他应用不能使用该端口
  • 在windows上查看端口占用情况
netstat -aon|findstr "49157"

8.3 TCP 协议 和 UDP协议

8.3.1 TCP 协议

  • TCP(Transmission Control Protocol)是一种可靠的、面向连接的协议(eg:打电话)
  • 特点 : 可靠,传输效率低,使用全双工通信(发送缓存&接收缓存)面向连接,流式的,面向字节流。
  • 使用TCP的应用:Web浏览器;电子邮件、文件传输程序。
8.3.1.1三次握手

发生于建立连接的时候

  • 第一次握手:客户端发送syn包(seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
  • 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。

简述三次握手:

  • 发生于建立连接的时候,客户端(client)向服务端(server)发送了一个syn请求
  • 客户端(client)得到服务端(server)的ack响应的同时还会收到一个由server端发来的syn链接请求
  • 客户端(client)进行回复ack后就建立起了一个tcp协议的链接
    注 : 三次握手的过程再代码中是由accept和connect共同完成的,具体的细节再socket中没有体现出来
8.3.1.1四次挥手

断开连接的时候

  • 第一次挥手:主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当 然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。
  • 第二次挥手:被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号)。
  • 第三次挥手:被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
  • 第四次挥手:主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。
1.在建立起连接之后 , 发送的每一条信息都有回执 , 为了保证数据的完整性,还有重传机制
2.长连接 :会一直占用双方的端口
3.IO(input,output)操作,输入和输出是相对内存来说的
        # write send - output
        # read recv   - input
4.能够传递的数据长度几乎没有限制

简述四次挥手:

  • server和client端对应的在代码中都有close方法
  • 一端发起的close操作都是一次fin的断开请求,得到'断开确认ack'之后,就可以结束一端的数据发送
  • 如果两端都发起close,那么就是两次请求和两次回复,一共是四次操作
  • 可以结束两端的数据发送,表示链接断开了

8.3.2 UDP协议

  • UDP(User Datagram Protocol)不可靠的、无连接的服务,
  • 特点 : 传输效率高(发送前时延小),一对一、一对多、多对一、多对多、面向数据报,尽最大努力服务,无拥塞控制。可能会丢消息 , 能够传递的数据的长度是有限的,是根据数据传递设备的设置有关系.
  • 使用UDP的应用:域名系统 (DNS);视频的在线观看;IP语音(VoIP)

8.4 osi七层模型及五层模型


各层功能定义(以公司A和公司B的一次商业报价单发送为例子进行讲解)

 

含义

例子

一.应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

实际公司A的老板就是我们所述的用户,而他要发送的商业报价单,就是应用层提供的一种网络服务,当然,老板也可以选择其他服务,比如说,发一份商业合同,发一份询价单,等等。

二.表示层

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

由于公司A和公司B是不同国家的公司,他们之间的商定统一用英语作为交流的语言,所以此时表示层(公司的文秘),就是将应用层的传递信息转翻译成英语。同时为了防止别的公司看到,公司A的人也会对这份报价单做一些加密的处理。这就是表示的作用,将应用层的数据转换翻译等。

三.会话层

会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

会话层的同事拿到表示层的同事转换后资料,(会话层的同事类似公司的外联部),会话层的同事那里可能会掌握本公司与其他好多公司的联系方式,这里公司就是实际传递过程中的实体。他们要管理本公司与外界好多公司的联系会话。当接收到表示层的数据后,会话层将会建立并记录本次会话,他首先要找到公司B的地址信息,然后将整份资料放进信封,并写上地址和联系方式。准备将资料寄出。等到确定公司B接收到此份报价单后,此次会话就算结束了,外联部的同事就会终止此次会话。

四.传输层

传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

传输层就相当于公司中的负责快递邮件收发的人,公司自己的投递员,他们负责将上一层的要寄出的资料投递到快递公司或邮局。

五.网络层

本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。ipv4 ipv6

网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说,从深圳发往北京的顺丰快递(陆运为例啊,空运好像直接就飞到北京了),首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。

六.数据链路层

将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。


七.物理层

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

快递寄送过程中的交通工具,就相当于我们的物理层,例如汽车,火车,飞机,船。


8.5 socket (套接字)

8.5.1 什么是socket


Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。帮助我们完成了所有信息的组织和拼接

socket历史

同一台机器上的两个服务之间的通信的

基于文件

基于网路的多台机器之间的多个服务通信

8.5.2基于基于TCP协议的socket


#sever.py文件
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',9000))

sk.listen(n)    # n 允许多少客户端等待
while True:   # outer
    conn,addr = sk.accept()    # 阻塞
    print(addr)
    while True:   # inner
        msg = conn.recv(1024).decode('utf-8')
        if msg.upper() == 'N': break
        print(msg)
        inp = input('请输入:')
        conn.send(inp.encode('utf-8'))
        if inp.upper() == 'N':
            break
    conn.close()
sk.close()


#client.py文件
import socket

sk = socket.socket()

sk.connect(('127.0.0.1',9000))
while True:
    inp = input('请输入:')
    sk.send(inp.encode('utf-8'))
    if inp.upper() == 'N':
        break
    msg = sk.recv(1024).decode('utf-8')
    if msg.upper() == 'N': break
    print(msg)
sk.close()


#client2.py文件
import socket

sk = socket.socket()

sk.connect(('127.0.0.1',9000))
while True:
    inp = input('请输入:')
    sk.send(inp.encode('utf-8'))
    if inp.upper() == 'N':
        break
    msg = sk.recv(1024).decode('utf-8')
    if msg.upper() == 'N': break
    print(msg)
sk.close()

8.5.3基于基于UDP协议的socket


#server.py文件
import socket
sk = socket.socket(type = socket.SOCK.DGRAM)
sk.bind(('127.0.0.1',9000))

while True:
    msg,client_addr = sk.recvfrom(1024)
    print(msg.decode('utf-8'))
    msg = input('请输入:').encode('UTF-8')
    sk.sendto(msg,client_addr)
sk.close()


#client.py文件
import socket

sk = socket.socket(type = socket.SOCK.DGRAM)
while True:
    msg = input('请输入:').encode('UTF-8')
    sk.sendto(msg,('127.0.0.1',9000))
    msg = sk.recv(1024)
    print(msg.decode('utf-8'))
sk.close()

8.5.4 阻塞和非阻塞

8.5.4.1 阻塞
  • cpu不工作
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。调用线程只有在得到结果之后才会返回。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
8.5.4.2非阻塞 I/O模型 :
  • 起因
    由于普通的server-socket一次只能处理一个client-socket.(因为socket在accept等待接收和recv等待数据数据时都会阻塞, 每次处理client只能应对一个socket.)
  • 基本结构
#server端
import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8888))
sk.setblocking(False)   # 将socket设置为非阻塞. 在创建socket对象后就进行该操作.括号内不写为True
sk.listen()

conn_1 = []
del_l = []
while True:
    try:
        conn,addr = sk.accept()   #阻塞,知道一个客户端来连接
        print(conn)  #如果成功就打印
        conn_1.append(conn)
    except BlockingIOError:
        for i in conn_1:    #跑完后对方断开连接,故服务端从已关闭的连接请求信息,在windows操作系统中会得到空解(而在ios中会报错)
            try:
                msg = i.recv(1024)
                if not msg:
                    del_l.append(i)
                    continue  
                print(msg)
                i.send(msg.upper()) 
            except BlockingIOError:
                pass
            for i in del_l:
                conn_1.remove(i)
            del_l.clear()
                
#conn.close()
sk.close()

#*********************************************************
#cliert1端
import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',8888))
           
for i in range(20):
 sk.send(b'alexsb')
 msg = sk.recv(1024)
    print(msg)
    time.sleep(0.5)
      
sk.close()

#*********************************************************
#cliert2端
import socket,time
sk = socket.socket()
sk.connect(('127.0.0.1',8888))
           
for i in range(20):
 sk.send(b'wusir')
 msg = sk.recv(1024)
    print(msg)
    time.sleep(0.5)
           
           
sk.close()

socket的非阻塞 i/o 模型 + io多路复用实现

  • 虽然非阻塞提高了cpu利用率,但费耗cpu做了很多无用功

8.6黏包

bytes str ,utf-8 和gbk有什么关系?

  • 计算机上的存储 / 网线上数据的传输 --> 二进制
  • 而对于字符串的存储是以字节形式
  • 计算机中0或1是1 位 bit 比特,而8位1个字节
# send
# str-encode(编码)->bytes

# recv
# bytes-decode(编码)-> str

8.6.1黏包问题的发现


同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种现象就是黏包。

注意:只有TCP有粘包现象,UDP永远不会粘包

8.6.2黏包问题的成因

tcp协议的拆包机制

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。

基于tcp协议特点的黏包现象成因

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束 , 此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

总结:

黏包现象只发生在tcp协议中:

1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。

  • 发生在发送端的粘包

    由于两个数据的发送时间间隔短+数据的长度小 , 为了减少tcp协议中的“确认收到”的网络延迟时间 , 所以由tcp协议的优化机制将两条信息作为一条信息发送出去了.

  • 发生再接收端的粘包

    由于tcp协议中所传输的数据无边界,所以来不及接收的多条数据会在接收放的内核的缓存端黏在一起

2.实际上,本质还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

8.6.3黏包问题的解决

  • 解决方案一:
    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。


    存在的问题:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
  • 解决方案二:
    我们可以借助一个模块,这个模块可以把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息之前只要先 接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就停止,就能刚好不多不少的接收完整的数据了。
struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes




# client
import socket
import struct

sk = socket.socket()
sk.connect(('127.0.0.1',9999))

msg = input('请输入:').encode('utf-8')
byte_len = struct.pack('i',len(msg)
sk.send(byte_len)
sk.send(msg)

msg = b'everyboday'
byte_len = struct.pack('i',len(msg)
sk.send(byte_len)
sk.send(msg)

sk.close()

# server
import socket
import struct

sk = socket.socket()
sk.bind(('127.0.0.1',9999))
sk.listen()

conn,addr = sk.accept()
byte_len = conn.recv(4)
size = struct.unpack('i',byte_len)[0]    # 得到一个元组,所以要索引
msg1 = conn.recv(size)
print(msg1)

byte_len = conn.recv(4)
size = struct.unpack('i',byte_len)
msg2 = conn.recv(size)
print(msg2)

conn.close()
sk.close()

8.7 验证客户端的合法性


8.7.1使用hashlib模块加密

#server
import hashlib,os,socket

def get_md5(secret_key,randseq):   #md5加密模块
    md5 = hashlib,md5(secret_key)
    md5.update(randseq)
    obj = md5.hexdigest()
    return obj

def chat(conn):
    while True:
        msg = conn.recv(1024).decode('utf-8')
        print(msg)
        conn.send(msg.upper().encode('utf-8'))
        
        
k = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()

secret_key = b'alexsb'   #加盐密码
while True:
    conn,addr = sk.accept()
    randseq = os.urandom(32)   #随机生成一个32位字节
    conn.send(randseq)
    md5code = get_md5(secret_key,randseq)
    ret = conn.recv(32).decode('utf-8')
    print(ret)
    if ret == md5code:
        print('是合法的客户端')
        chat(conn)
    else:
        print('不是合法的客户端')
        conn.close()
sk.close()


#client
import hashlib
import socket
import time

def get_md5(secret_key,randseq):
    md5 = hashlib.md5(secret_key)
    md5.update(randseq)
    res = md5.hexdigest()
    return res

def chat(sk):
    while True:
        sk.send(b'hello')
        msg = sk.recv(1024).decode('utf-8')
        print(msg)
        time.sleep(0.5)

        
sk = socket.socket()
sk.connect(('127.0.0.1',9000))

secret_key = b'alexsb'
randseq = sk.recv(32)
md5code = get_md5(secret_key,randseq)
sk.send(md5code.encode('utf-8'))
chat(sk)

sk.close()

8.7.2使用hmac模块加密

#server
import os
import hmac
import socket

def get_md5(secret_key,randseq):
    h = hmac.new(secret_key,randseq)
    res = h.digest()
    return res

def chat(conn):
    while True:
        msg = conn.recv(1024).decode('utf-8')
        print(msg)
        conn.send(msg.upper().encode('utf-8'))

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()

secret_key = b'alexsb'
while True:
    conn,addr = sk.accept()
    randseq = os.urandom(32)
    conn.send(randseq)
    hmaccode = get_md5(secret_key,randseq)
    ret = conn.recv(16)
    print(ret)
    if ret == hmaccode:
        print('是合法的客户端')
        chat(conn)
    else:
        print('不是合法的客户端')
        conn.close()
sk.close()


#client
import socket
import time
import hmac

def get_md5(secret_key,randseq):
    h = hmac.new(secret_key,randseq)
    res = h.digest()
    return res
def chat(sk):
    while True:
        sk.send(b'hello')
        msg = sk.recv(1024).decode('utf-8')
        print(msg)
        time.sleep(0.5)

sk = socket.socket()
sk.connect(('127.0.0.1',9000))

secret_key = b'alexsb'
randseq = sk.recv(32)
hmaccode = get_md5(secret_key,randseq)

sk.send(hmaccode)
chat(sk)

sk.close()

8.8 socketserver

socketserver模块,直接实现tcp协议可并发的server端


#server
import socketserver

class Myserver(socketserver.BaseRequestHandler):
    def handle(self):  # 自动触发了handle方法,并且self.request == conn
        msg = self.request.recv(1024).decode('utf-8')
        self.request.send('1'.encode('utf-8'))
        msg = self.request.recv(1024).decode('utf-8')
        self.request.send('2'.encode('utf-8'))
        msg = self.request.recv(1024).decode('utf-8')
        self.request.send('3'.encode('utf-8'))

server = socketserver.ThreadingTCPServer(('127.0.0.1',9000),Myserver)
server.serve_forever()

#*************************************************************
#client1
import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
for i in range(3):
    sk.send(b'hello,yuan')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(1)

sk.close()

#*************************************************************
#client2
import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
for i in range(3):
    sk.send(b'hello,wusir')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(1)

sk.close()

#*************************************************************
#client3
import socket
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
for i in range(3):
    sk.send(b'hello,alex')
    msg = sk.recv(1024)
    print(msg)
    time.sleep(1)

sk.close()