10-02 网络编程

一. C/S架构和B/S架构

# 互联通信软件有两种模式:CS架构和BS架构
    - CS指的是Client-Server,分别有一个客户端软件和一个服务端软件
    - BS指的是Browser-Server,一个浏览器和一个服务端软件

# 客户端与服务器是怎么通信基本三层结构:
客户端软件send                 服务端软件recv
操作系统                       操作系统
计算机硬件 <====物理介质=====>  计算机硬件

二. OSI七层

须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(打个单机游戏,玩个扫雷啥的)

如果你要跟别人一起玩,那你就需要上网了,什么是互联网?

互联网的核心就是由一堆协议组成,协议就是标准,比如全世界人通信的标准是英语

如果把计算机比作人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都就可以按照统一的标准去收发信息从而完成通信了。

人们按照分工不同把网络协议从逻辑上划分了层级:

详见网络协议: https://www.cnblogs.com/yang1333/articles/12716113.html

三. socket层: 了解socket是什么?

上图分析:

介绍: 在了解了osI基层协议之后,我们看到,应用层与传输层之间,有着一个socket的抽象层,这里的抽象层并不存在于osI七层协议之中,这里的socket抽象层是为应用层通过下面所有层次以后再通过网络通信的一种接口.

socket是什么?
    - Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
    - 所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

套接字socket封装的好处:
    - 对应用程序的开发者来说,只需要关注应用中相关的事情,应用层你想怎么处理封装你的数据, 在应用层定制什么样的协议, 你想干什么就干什么,只需要把应用层处理好的数据交给socket,它会帮你处理到传输层,到网络层, 到数据链路层, 到物理层所需要干的事情.

研究套接字socket抽象层次的目的是什么?
    - socket抽象层不是去帮你写应用程序的,主要是把你应用程序所写好的数据基于网络协议发出去

四. 套接字发展史及分类

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

基于文件类型的套接字家族:

套接字家族的名字:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

基于网络类型的套接字家族:

套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET)

总结:

最早的socket不是用来网络通信的, 是为了处理,同一台机器之上的两个进程之间互相的通信.
    - 原来的两个进程是不属于同一个内存空间,内存空间之间互相隔离.怎么解决这个问题呢?
    - 解决: 硬盘之间的数据是互相共享的,一个进程把数据写入到文件里面去,让另一个进程通过读取文件来达到数据之间的互相通信的目的.

五. 套接字工作流程

客户端:

socket()
    客户端所在的应用层拿到抽象层中的socket指定直接打交道的下一层的传输层的协议.(这里以TCP/UPD为例) 
connect()      
    接着通过拿到TCP服务端的IP和端口,找到服务端主机上与之通信的那个独一无二的程序.(提示:客户端不需要绑定,客户端的端口,在访问服务端时服务端就能拿到客户端的端口,因此客户端端口不需要绑定)
read()和write()
    与服务端链接成功以后,就可以对服务端进行收发数据操作.
close()
    最后, 向操作系统发送系统调用关闭客户端与服务端之间的连接通路,并回收建立连接时所占用的系统资源,

服务端:

socket()
    服务端指定socket与传输层打交道的协议下一层的传输层的协议.(这里以TCP/UPD为例)
bind()       
    服务端需要绑定Ip和端口,等客户端通过这个Ip和端口, 可以找到全世界独一无二的在这个服务器上的这个程序, 并与之通信.
listen()
    三次握手建立链接之前,服务端本身就处于LISHEN状态,此时服务端的链接还在半连接池中,还没有被客户端所进行链接
accept()
    它其实就是Tcp建立链接三次握手的地方,如果有请求,则会去listen的半连接池中取获取连接. 它是基于串联的一个一个的服务,一个服务结束了以后才能进行下一个服务
read()和write()
    客户端建立成功以后, 然后服务端就可以对客户端进行收发数据.
close()
    最后, 向操作系统发送系统调用关闭服务端与客户端之间的连接通路,并回收建立连接时所占用的系统资源,    

六. 基于TCP的套接字

注意!!!: TCP是基于链接通信的,所以必须先启动服务端,然后再去客户端中链接服务端,

TCP服务端

"""
from socket import *
server = socket(family=AF_INET, type=SOCK_STREAM)
    第一个参数: 指定套接字家族中的工作类型. 
        AF_INET: 指定基于网络通信的套接字类型.(补充: AF全称Address Family)
        
    第二个参数: 指定的是让socket这个抽象层与下一层传输层所打交道的协议类型.
        SOCK_STREAM: 这是代指的是一种流式协议,流式协议指的就是TCP协议.
    
server.bind(('IP', PORT))   
    1) 第1个参数指定IP,字符串格式.
    2) 第2个参数指定端口,(端口范围0~65535. 其中0~1023都是公认端口, 也叫熟知端口. 最好不要使用, 不然会端口冲突.)    
    补充: 
        - 服务端绑定的Ip和端口, 其中的Ip地址是自己局域网中的Ip地址. 要想被其他局域网中的主机进行访问,那么得在公网申请一个地址,把公网地址与本服务器的地址绑定一种映射关系. 
        - 0.0.0.0: 这种IP地址能被任何的机器所访问到, 对本机来说,它就是一个“收容所”,所有不认识的“三无”人员,一律送进去。(详细网址: https://baike.baidu.com/item/0.0.0.0/7900621?fr=aladdin)
        - 127.0.0.1: 这种地址是回环测试地址,这个地址只能自己本机访问, 就算在同一个局域网中另一台主机也访问不到.
        - 在bind操作之前有一种命令setsockopt可以,重新利用你的端口(用于测试)
        
server.listen(5)
    注意!!! 这里获取的是连接请求, 不是建立成功以后的连接.
    1) 这里参数指定半连接池的大小(int类型). 我这里指定的是5, 这里可以被6个客户端进行连接请求, 当第7个客户端进行连接所就不能成功获取半连接池中的链接请求. 
    2) 意义: 使用这种半连接池,可以合理的去控制你服务端被客户端所访问连接请求的个数,进而控制客户端对服务端资源的访问, 防止服务端被客户端访问资源过多,造成服务端撑死.
    3) 表现: 目前这种情况,当有一个客户端以及链接成功, 服务端在为这个客户端提供服务. 如果有5个客户端发起链接请求,那么这5个客户端,会进行等待(因为这里没有实现并发),等待上一个客户端断开, 好与服务端的accept建立连接. (注意!!: 服务端正在为客户端提供通讯循环的那个客户端,也算作一次链接请求,所以一共是6次,这里的半连接池只能支持6个客户端发起连接请求.)
    
conn, client_addr = server.accept()
    1) accept: 这里是等待客户端发送syn链接请求, 没有客户端进行链接,这里会堵塞住, 处于一种等待的状态.
    2) conn: 这是一个属于客户端的套接字对象,通过拿到这个套间之对象与客户端进行通信.
    3) client_addr: 返回值是一个元组形式
        - 元组中第1个参数,是服务端与客户端建立双向链接通路的一个对象.
        - 元组中第2个参数, 是拿到客户端的Ip和端口,它们两个也是在包含在一个人组中.
    注意: 如果客户端和服务端都在同一台主机之上测试运行,服务端拿到的client_addr中的Ip和端口. 其中的端口和服务端的端口一定不一样,因为同一台主机之上的端口是独一无二的.
    
data = conn.recv(1024): 这里指定最大接收的数据量是1024个字节(Bytes). 返回的是一个bytes类型. (注意: 这个数不能超过内存的字节接收大小,数据的处理都是在内存之中中转的,所以不能超过.)
        
conn.send(bytes类型的数据): 这里指定发送bytes类型的数据, 往conn这个通向客户端的连接通路中发送数据.          

conn.close(): 当客户端发送FIN结束请求时,这里要关闭该客户端的链接通路, 同时也回收占用在服务端上之前申请的与客户端的系统的连接资源.(必选操作)

server.close(): 这里是关闭服务端的系统进程(可选操作: 因为服务器不应该被关闭,应该要一直在运行.)
    补充: 这里关闭了服务端,绑定的IP和端口如果关闭了以后,操作系统需要回收这个资源,回收这个资源需要一定的时间,所以如果需要再次运行上面的bind(), 则端口需要重新指定,让操作系统为上一次指定的端口申请的系统资源的回收腾出时间.
"""

import socket

server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8080))
server.listen(5)


print("等待客户端发送syn请求建立双向连接通信中.....")
while True:  # 连接循环
    conn, client_addr = server.accept()
    while True:  # 通讯循环
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:  # 这里解决的是linux系统中的异常,
                break
            conn.send(data_bytes.upper())
        except ConnectionResetError as e:  # 这里解决的是windows系统中的异常,
            # print('conn:', conn)
            print('client_addr:', client_addr)
            print(e)
            break
    conn.close()


server.close()

TCP客户端

"""
from socket import *
client = socket(family=AF_INET, type=SOCK_STREAM)

client.connect(('IP', PORT)): 这里指定的是你需要去连的服务端的IP和端口.

client.send(bytes类型数据): 这里要指定bytes类型数据,往双向通路中的管道 服务端1 --> 客户端1 的管道中传输数据. 

data = client.recv(1024) 

client.close(): 这里是客户端向服务端发送FIN结束请求, 同时也回收占用在客户端上系统的连接资源.(必选操作)
"""

import socket

client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))

while True:
    msg = input("输入要发送的消息: ").strip()
    if not msg:   # 这里解决的是客户端发送空数据, 而客户端操作系统缓存中并没有数据发送给服务端, 因而服务端并不能从服务端的操作系统缓存中获取数,因此服务端和客户端同时处在一个recv堵塞的状态,
        continue
    client.send(msg.encode('utf-8'))
    data_bytes = client.recv(1024)
    print(data_bytes)
client.close()

总结

1) 引发阻塞操作是哪几种操作?
    accept, recv, send: 其中明显引发阻塞操作的是accept, recv.
    
2) 服务端需要处理客户端发送空数据的问题.(引发原因: 因为send可以发空, 而recv不能收空.)
    - 引发问题: 如果客户端发空, 服务端就一直在堵塞在原地,而这个时候客户端,也收不到服务端所发过来的数据,也同时堵塞在原地.
    - 解决: 客户端要进行判断,让客户端输入内容不能为空,
    - 底层原理: 客户端的send操作与服务端的recv操作并不是一对应的. 客户端与服务端的收发都是针对自己. 
        send: 把数据发送到操作系统的缓存中,向操作系统发起系统调用,最终让操作系统下发到传输层 -> 网络层 -> 数据链路层 -> 物理层,最后交给网卡这个硬件发送出去.   
        recv: 从操作系统的缓存中读取数据,客户端如果发送空数据, 在客户端的操作系统缓存中并没有数据. 而服务端的操作系统的缓存中更加没有数据, 所以服务端就会一直recv堵塞等待客户端的数据.  
        
3) 客户端强行终止连接, 服务端会出现异常. 如果是在windows系统中,会抛出异常(ConnectionAbortedError). 如果是在linux系统中recv会一直接收空,从而进入死循环.
    - 本来客户端与服务端是一个正常的链接,客户端没有和服务端进行商量,客户端强行断开链接,而服务端的conn这个套接字对象以为客户端的失效的链接还是正常的,还是在基于一个正常的行为去接收客户端的数据.
    - 举个例子: 基于TCP协议通信的客户端与服务端之间建立连接以后两者之间了搭建了一个桥墩子,如果客户端把桥墩子炸了,服务端也就炸了, 所以我们要提供解决的办法, 让服务器不受影响.
    - 解决linux系统中的异常:  在服务端中判断接收的数据是否为空.(注意: 这里的空并不是客户端发送空数据服务端所接收到的空,而是客户端强行终止链接以后,linux系统中引发的一种错误,)
    - 解决window系统中的异常: 使用异常处理机制.
  	- 补充!!!: 还有另一种情况就是客户端正常使用close()断开连接, 在windows系统中的服务端也会收到空的数据.
    
4) 解决客户端强行终止链接以后, 让服务端已处于一直提供服务的状态.
    - 问题: 这种通信循环外面套外循环的这种链接循环只是一种,1对1的服务,不能分心,只有等上一个链接的客户端,断开以后,下一个链接的客户的连接才能建立. 
    - 举个例子: 连接循环就像一个,拉客的,通信循环是接客的,拉客只需要有一个就行了,而我们的接客的,要需要很多个,才能为很多的客户端进行服务,这个时候我们就需要用到并发.
    - 解决: 在客户端的通信循环的基础之上,在外面再套一层接收客户端链接的while循环, 达到链接循环的目的.    

七. 基于UDP的套接字

注意!!!: UDP是无链接的,先启动哪一端都不会出问题.

UDP服务端

"""
server = socket(family=AF_INET, type=SOCK_DGRAM)
     SOCK_DGRAM: 这里代指的是一种数据报协议,数据报协议指的就是udp协议(补充: 数据报就是自己utp协议中有自己的头,有自己的数据部分)

server.bind('IP', PORT)

bytes类型的数据, client_addr = server.recvfrom(1024)
    - client_addr是一个2元组的形式: 第一个参数是客户端的IP地址, 第二个参数是客户端发送数据进程软件的端口号.

server.sendto(bytes类型处理过后的数据, client_addr)

server.close()
"""

import socket

server = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))
while True:
    print("等待客户端进行访问......")
    data_bytes, client_addr = server.recvfrom(1024)
    server.sendto(data_bytes.upper(), client_addr)
    print('data_bytes:', data_bytes)
    print('client_addr:', client_addr)
server.close()

UDP客户端

"""
from socket import *

client = socket(family=AF_INET, type=SOCK_DGRAM)

client.sendto(bytes类型的数据, ('服务端IP', 服务端端口))

data_bytes, client_addr = client.recvfrom(1024)

client.close()
"""

import socket

client = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
while True:
    msg = input("输入要发送的消息: ").strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data_bytes, server_addr = client.recvfrom(1024)
    print('data_bytes:', data_bytes)
    print('server_addr:', server_addr)
client.close()

总结: UDP与TCP中的2点区别

注意1: UDP协议不会因为客户端发送的数据为空,从而导致客户端和服务端发生异常. 
注意2: UDP协议服务端不会因为客户端强制断开连接,从而导致服务端发生异常. 
    - UDP协议叫数据报协议,什么叫数据报?报就分成头和数据两部分, 它是一个完整的整体. 它不是单纯的数据. 
    - 举个例子: 基于UDP协议发送的数据, 每次的发都是一个集装箱过去,并不是空的,所以,你的数据看起来是空,但是我会在数据报的基础上,对你的数据进行一个处理,所以说服务端收到的并不是空. 
    - 数据报的概念: 当客户端发送的数据虽然是空,但是数据报会以一个集装箱的样子给你发送到服务端过去,因此服务端收到的,其实并不是空的数据, 服务端收到的还有客户端的Ip和端口.

基于UDP协议实现聊天功能: 注意!!! 聊天是客户端与客户端进行的聊天,客户端把数据发送到了服务端,再由服务端转发转发到客户端中, 这样就实现了客户端与客户端之间进行的聊天.

八. 套接字方法及函数介绍

1. 服务器端套接字

s.bind() 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
s.listen() 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
s.accept() 被动接受TCP客户端连接,(阻塞式)等待连接的到来

2. 客户端套接字

s.connect() 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

3. 公共用途的套接字函数

s.recv() 接收TCP数据,数据以字符串形式返回,缓冲区(bufsize)指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
s.send() 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小(提示: 在输入空的时候小于)。
s.sendall() 完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
s.recvfrom() 接收UDP数据,与recv()类似,但返回值是(data_bytes,address)。其中data_bytes是接收的bytes类型的数据,address是发送数据的地址, 以元组('IP', port)形式表示。
s.sendto() 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
s.makefile() 创建一个与该套接字相关连的文件

九. 粘包现象

1. 基于TCP协议实现远程命令的黏包问题

TCP服务端:

import socket
import subprocess

server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 在bind前加, 方便我们测试的时候可以重用端口.
server.bind(("127.0.0.1", 8080))
server.listen(5)
while True:  # 连接循环
    conn, client_addr = server.accept()
    print("有客户端建立连接........")
    while True:  # 通讯循环
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:
                # 这里解决的问题是:
                """
                当你的程序运用在linux系统中,客户端强制断开链接,服务端会收到空的数据,在这种情况下就会退出与当前客户端的通讯循环,并关闭与该客户端的连接, 并回收当前客户端在服务端当前所占的系统资源,
                还有另一种情况就是客户端正常使用close()断开连接, 服务端也会收到空的数据,
                """
                break
            obj = subprocess.Popen(
                data_bytes.decode('utf-8'),
                shell=True,
                stderr=subprocess.PIPE,
                stdout=subprocess.PIPE,
            )
            # 注意!!!: subprocess如果执行的是'tasklist'命令, read获取正确结果要在错误结果之前(补充: 具体为什么目前我也不知道,不过执行其他的命令没有这种要求,)
            stdout, stderr = obj.stdout.read(), obj.stderr.read()

            # 发送数据
            # conn.send(stdout + stderr)  # 注意!!!: 这样做属于字符串的拼接,会重新申请内存空间.
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            # 这里解决的问题是:
            """
            当你的程序运行在windows系统中,客户端强制断开链接,服务端就会引发异常. 因此,就可以使用异常处理机制来监测这种异常,此时一旦监测出这种异常,就代表客户端强制断开了链接.
            在这种情况下就会退出与当前客户端的通讯循环,并关闭与该客户端的连接, 并回收当前客户端在服务端当前所占的系统资源,
            """
            break
    conn.close()
server.close()

TCP客户端:

import socket

client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    cmd = input("输入命令: ").strip()

    if not cmd:
        # 这里解决的问题:
        """
        当客户端输入为空的时候,在客户端的操作系统缓存中,并没有数据能发送给服务端. 硬件不能识别你这个空的数据,只有我们这种逻辑层面才有这种空的数据的存在.
        因此服务端的recv操作会在服务端的操作缓存中拿取数据,但是与此同时,服务端的操作系统缓存中并没有数据,于是服务端一直在recv堵塞状态.
        接着客户端因为send发送了数据,处于recv等待服务端发送回来的数据的状态,因而两者会处于一种recv堵塞的状态,
        """
        continue
    client.send(cmd.encode('utf-8'))

    # 1. 粘包问题出现:
    """
    问题1: 服务端发送回来的数据很多收不完,剩下的数据会保存在客户端的操作系统缓存中并不会丢失.
    问题2: 有可能服务端的数据过大,服务端并不是一次性的给你发送过来,可能分多次发送过来,所以这里接收的话可能接收的是部分.(这里是服务端的问题)
    """
    # data_bytes = client.recv(1024)


    # 2. 粘包问题出现的原因:
    """
    1. Tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
    2. 收数据没有收干净有残留, 就会与下次结果混淆在一起.
    """

    # 3. 解决核心: 保证每次都收干净, 不要任何残留
    """
    服务端发送给客户端所需要发送数据的总大小,客户端拿到这个总大小,然后进行循环接收, 直至收取完毕, 结束本次收取数据的循环.
    """

    # 4. 解决问题: 尽最大量的收取客户端传输过来的数据量.
    # 缺点: 操作系统缓存有一定的容量,如果服务端发送过的数据量过大,本地操作系统缓存并不能一次性容纳,服务端的数据将会继续发送,因此,这种时候也不是一个好的解决方案,收取的数据也不一定收取全.
    data_bytes = client.recv(8096)
    print(data_bytes.decode('utf-8'))

client.close()

2. 基于UDP协议没有黏包问题

注意!!!:

在windows系统中,如果发送方的数据大于接收方的数据,就会抛出异常,接收不了. (OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。)

在linux系统中,可以去接收,但是只会接收一部分数据,多出来的数据会丢失.

TCP服务端:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8080))
res1 = server.recvfrom(2)  # b"hello"
print(res1)
res2 = server.recvfrom(3)  # b"world"
print(res2)

server.close()

UDP客户端:

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'hello', ('127.0.0.1', 8080))
client.sendto(b'world', ('127.0.0.1', 8080))

client.close()

总结:

基于UDP协议通讯的套接字程序, 发送段的一个sendto,对应接受端的一个recv_from, 含有一一对应的关系. 如果发送到的数据量,大于接收端接收数量, 多出的部分接收端并不会接收会直接丢弃.
补充: 
    提示: 一个utp协议稳定传输的字节数是512字节.
    DNS服务器为什么只有13台?不能多造? 因为早期DNS在设计的时候,DNS的查询用的就是utp协议, 而utp协议一次有效传输的数据量最大就是512字节,这512字节里面既要包含你utp协议的查询信息, 还要的带着13台DNS的查询的信息. 因此13台已经是最大的极限了, 如果再多一台, udp协议包含的DNS的查询信息的数据量就超了,再发的时候就不稳定了. 

十. 什么是粘包 ==> nagle算法

强调!!!: 只有TCP有粘包现象,UDP永远不会粘包

# TCP黏包问题与接收方的关系:(主要)
"""
接收方不知道消息之间的界限, 不知道一次性向本地的操作系统资源中取多少字节的数据,
"""

# TCP黏包问题与发送方的关系:(次要)
"""
发送方引起的黏包是由TCP协议本身造成的,TCP为了提高传输效率,发送方往往要收集到足够多的数据,才发送一个TCP段. 若持续几次需要send的数据都很少,通常TCP会根据优化算法(nagle)把这些数据合并成一个TCP段一次发送出去,这样就无形之间,让接收方收到了黏包的数据.
"""

# TCP两种情况下会发生粘包
"""
1. 发送端需要等缓冲区满才发送出去,造成黏包. 因为此时发送的数据时间间隔很短,数据很小,由nagle算法计算以后,会合到一起, 从而造成黏包
2. 接收方不及时接收缓冲区的包,造成多个包收. 是由于服务端发送了一段大数据,客户端只收了一小部分,客户端下次再收的时候,还是从缓冲区拿上次遗留的数据, 从而产生粘包.
"""

十一. 解决粘包的low比处理方法

十二. 小洋解决粘包的方法

1. struct模块

只需要知道下面这两种struct.pack, struct.unpack我们就可以解决这种面包的问题,更详细的了解struct用法网址: https://docs.python.org/zh-cn/3/library/struct.html#struct.calcsize

函数介绍

import struct
"""
struct.pack(format, v1, v2, ...)
返回一个 bytes 对象,其中包含根据格式字符串 format 打包的值 v1, v2, ... 参数个数必须与格式字符串所要求的值完全匹配。
"""
bytes = struct.pack('i', 123456789)
print(bytes)  # b'\x15\xcd[\x07'


"""
struct.unpack(format, buffer)
根据格式字符串 format 从缓冲区 buffer 解包(假定是由 pack(format, ...) 打包)。 结果为一个元组,即使其只包含一个条目。 缓冲区的字节大小必须匹配格式所要求的大小,如 calcsize() 所示。
"""
tuple_res = struct.unpack('i', bytes)
print(tuple_res)  # (123456789,)
number = tuple_res[0]
print(number)  # 123456789


"""
struct.calcsize(format)
返回与格式字符串 format 相对应的结构的大小(亦即 pack(format, ...) 所产生的字节串对象的大小)。
"""
print(struct.calcsize('i'))  # 4

格式字符

格式字符具有以下含义;C 和 Python 值之间的按其指定类型的转换应当是相当明显的。 ‘标准大小’列是指当使用标准大小时以字节表示的已打包值大小;也就是当格式字符串以 '<', '>', '!''=' 之一开头的情况。 当使用本机大小时,已打包值的大小取决于具体的平台。

格式 C 类型 Python 类型 标准大小 注释
x 填充字节
c char 长度为 1 的字节串 1
b signed char 整数 1 (1), (2)
B unsigned char 整数 1 (2)
? _Bool bool 1 (1)
h short 整数 2 (2)
H unsigned short 整数 2 (2)
i int 整数 4 (2)
I unsigned int 整数 4 (2)
l long 整数 4 (2)
L unsigned long 整数 4 (2)
q long long 整数 8 (2)
Q unsigned long long 整数 8 (2)
n ssize_t 整数 (3)
N size_t 整数 (3)
e (6) 浮点数 2 (4)
f float 浮点数 4 (4)
d double 浮点数 8 (4)
s char[] 字节串
p char[] 字节串
P void * 整数 (5)

在 3.3 版更改: 增加了对 'n''N' 格式的支持

在 3.6 版更改: 添加了对 'e' 格式的支持。

2. 解决黏包问题基础版

TCP服务端:

import socket
import subprocess

server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 在bind前加, 方便我们测试的时候可以重用端口.
server.bind(("127.0.0.1", 8080))
server.listen(5)
while True:  # 连接循环
    conn, client_addr = server.accept()
    print("有客户端建立连接........")
    while True:  # 通讯循环
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:
                break
            obj = subprocess.Popen(
                data_bytes.decode('utf-8'),
                shell=True,
                stderr=subprocess.PIPE,
                stdout=subprocess.PIPE,
            )
            # 注意!!!: subprocess如果执行的是'tasklist'命令, read获取正确结果要在错误结果之前(补充: 具体为什么目前我也不知道,不过执行其他的命令没有这种要求,)
            stdout, stderr = obj.stdout.read(), obj.stderr.read()

            # 1. 定制数据头部
            # 实现方式一:  缺点, 定制比较单一.
            import struct
            header_bytes = struct.pack('i', len(stderr) + len(stdout))
            conn.send(header_bytes)

            # 2. 发送数据
            # conn.send(stdout + stderr)  # 注意!!!: 这样做属于字符串的拼接,会重新申请内存空间.
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            break
    conn.close()
server.close()

TCP客户端:

import socket
import struct

client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    cmd = input("输入命令: ").strip()

    if not cmd:
        continue
    client.send(cmd.encode('utf-8'))

    # 解决问题: 使用struct自定义固定长度的数据头部, 客户端先拿到这种固定长度的数据,来获取服务端发送过来的数据量的大小,接着再通过循环收取数据,
    header_bytes = client.recv(4)
    data_length = struct.unpack('i', header_bytes)[0]

    total_size = data_length
    recv_size = 0
    while recv_size < total_size:
        data_bytes = client.recv(1024)
        recv_size += len(data_bytes)
        print(data_bytes.decode('gbk'))
    else:
        print()

client.close()

3. 解决黏包问题最终实现

TCP服务端:

import socket
import subprocess
import struct

server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 在bind前加, 方便我们测试的时候可以重用端口.
server.bind(("127.0.0.1", 8080))
server.listen(5)
while True:  # 连接循环
    conn, client_addr = server.accept()
    print("有客户端建立连接........")
    while True:  # 通讯循环
        try:
            data_bytes = conn.recv(1024)
            if not data_bytes:
                break
            obj = subprocess.Popen(
                data_bytes.decode('utf-8'),
                shell=True,
                stderr=subprocess.PIPE,
                stdout=subprocess.PIPE,
            )
            # 注意!!!: subprocess如果执行的是'tasklist'命令, read获取正确结果要在错误结果之前(补充: 具体为什么目前我也不知道,不过执行其他的命令没有这种要求,)
            stdout, stderr = obj.stdout.read(), obj.stderr.read()

            # 1. 定制数据头部
            # 实现方式二:
            '''
            1. 定制头部字典. 头部字典里面放,数据内容的大小,数据的描述信息,数据的md5值,等等数据,
            2. 接着将定制的头部json格式化成字符串,将字符串encode解码成bytes类型为了可以进行send操作
            3. 再使用struct将获取到的bytes数据的长度打包成固定长度, 这个第一个发送, 让客户端可以获取固定长度的bytes数据. 客户端获取到这个bytes类型的数据, 先decode解码, 再进行json反序列化拿到头部字典
            4. 客户端就可以通过获取到的头部字典拿到需要准备接收的数据的长度, 循环recv接收数据. 最终, 完美解决了黏包问题.
            '''
            dic = {
                'description': '这是一个执行终端命令的程序!',
                'data_size': len(stdout) + len(stderr),
                'md5': 'hf7wef09-n19in31hf98h1893 h89h1fh(这里瞎写的, 只是描述一下)',
            }
            import json
            dic_json = json.dumps(dic)
            dic_json_bytes = dic_json.encode('utf-8')
            dic_length = len(dic_json_bytes)
            header_bytes = struct.pack('i', dic_length)
            conn.send(header_bytes)
            conn.send(dic_json_bytes)

            # 2. 发送数据
            # conn.send(stdout + stderr)  # 注意!!!: 这样做属于字符串的拼接,会重新申请内存空间.
            conn.send(stdout)
            conn.send(stderr)
        except ConnectionResetError:
            # 解决的问题是:
            # 当你的程序运行在windows系统中,客户端强制断开链接,服务端就会引发异常. 因此,就可以使用异常处理机制来监测这种异常,此时一旦监测出这种异常,就代表客户端强制断开了链接.
            # 在这种情况下就会退出与当前客户端的通讯循环,并关闭与该客户端的连接, 并回收当前客户端在服务端当前所占的系统资源,
            break
    conn.close()
server.close()

TCP客户端:

import socket
import struct
import json

client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    cmd = input("输入命令: ").strip()

    if not cmd:
        continue
    client.send(cmd.encode('utf-8'))

    # 1. 接收头部, 解析头部, 获取有用信息
    """
    1. 使用struct获取自定义固定长度, 
    2. 再拿到bytes格式json格式的dic内容, 
    3. 接着通过decode解码, 
    4. 然后再使用json反序列化获取dic, 
    5. 再由dic拿到数据量的大小,接着再通过循环收取数据,
    """
    header_bytes = client.recv(4)
    dic_length = struct.unpack('i', header_bytes)[0]
    bytes_json_dic = client.recv(dic_length)

    json_dic = bytes_json_dic.decode('utf-8')
    dic = json.loads(json_dic)
    data_size = dic['data_size']

    # 2. 循环接收数据
    total_size = data_size
    recv_size = 0
    while recv_size < total_size:
        data_bytes = client.recv(1024)
        recv_size += len(data_bytes)
        print(data_bytes.decode('gbk'))
    else:
        print()

client.close()

十三. 认证客户端的链接合法性

十四. socketserver实现并发

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

1. 基于TCP协议的使用

TCP服务端:

"""
TCP服务端:
服务端要做两件事:
    - 第1件事就是循环地从半连接池中取出链接请求与其建立双向链接,拿到链接对象
    - 第2件事就是拿到链接对象,与其进行通信循环 ===> handle

import socketserver
class MyRequestHandle(socketserver.BaseRequestHandler):
    def handle(self): # 必须要写. 里面放的就是我们的通讯循环. 用来存放与客户端进行通信的逻辑代码.
        self.request
            - 如果是TCP协议,拿到的就是一个客户端的套接字对象conn
        self.client_address
            - 以('ip', 端口)二元组的形式表示, Ip和端口都是客户端的.

        补充: socketserver.ForkingTCPServer Windows系统中不支持.


server = socketserver.ThreadingTcpServer(('IP', 端口), 自定义的类)
    - 第1个参数: 指定服务端所绑定的Ip和端口, 这里的Ip和端口目的是提供客户端进行访问.
    - 第2个参数: 指定我们自定义的那个类里面定义我们的通信循环.

server.serve_forever()
    - 永久提供服务. 这是一个死循环的过程,对应的就是我们的链接循环.
"""

import socketserver


class MyRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)
        print(self.client_address)
        while True:
            try:
                data_bytes = self.request.recv(1024)
                if not data_bytes:
                    break
                self.request.send(data_bytes.upper())
            except ConnectionResetError:
                break
            except Exception:
                break
        self.request.close()


s = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandler)
s.serve_forever()

TCP客户端:

from socket import *

client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
    msg = input(">>: ").strip()
    if not msg:
        continue
    client.send(msg.encode("utf-8"))
    data_bytes = client.recv(1024)
    print(data_bytes)

2. 基于UDP协议的使用

UDP服务端:

"""
TCP服务端:
import socketserver
class MyRequestHandle(socketserver.BaseRequestHandler):
    def handle(self): # 必须要写. 里面放的就是我们的通讯循环. 用来存放与客户端进行通信的逻辑代码.
        self.request 拿到的是一个元组
            - 第1个参数是客户端发送过来的数据,
            - 第2个参数是拿到的客户端的套接字对象

        self.client_address
            - 以('ip', 端口)二元组的形式表示, Ip和端口都是客户端的.


server = socketserver.ThreadingUDPServer(('IP', 端口), 自定义的类)
- 第1个参数: 指定服务端所绑定的Ip和端口, 这里的Ip和端口目的是提供客户端进行访问.
- 第2个参数: 指定我们自定义的那个类类里面定义我们的通信循环.

server.serve_forever()
- 只负责循环的收,以后包括收发在内后续的数据,都交给线程去处理.
"""

import socketserver


class MyRequestHanlder(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)
        print(self.client_address)
        data_bytes, conn = self.request
        print(f'客户端发送过来的数据: {data_bytes.decode("utf-8")}')
        conn.sendto(data_bytes.upper(), self.client_address)


s = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyRequestHanlder)
s.serve_forever()

UDP客户端:

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 流式协议=》tcp协议

while True:
    msg = input('>>>: ').strip()
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    data_bytes, server_address = client.recvfrom(1024)
    print('data_bytes:', data_bytes)
    print('server_address:', server_address)
client.close()

十五. 补充: 解决服务端重启, 操作系统没来的及回收端口占用的系统资源导致端口无法重用

# 加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
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()        #关闭服务器套接字(可选)
posted @ 2020-04-16 22:22  给你加马桶唱疏通  阅读(220)  评论(0编辑  收藏  举报