26--网络编程:socket套接字编程

一 socket介绍

# Socket翻译为套接字
  是应用层与TCP/IP协议族通信之间的抽象层
  是一组接口,把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用

# 在设计模式中
  Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面
  对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议

二 套接字分类

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

    套接字家族的名字:AF_UNIX

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

    套接字家族的名字:AF_INET

    # AE_INET家族有很多地址家族
      AF_INET是使用最广泛的一个 
      由于我们只关心网络编程,所以大部分时候只使用 AF_INET
    

三 套接字工作流程

# 服务器端
  1.先初始化Socket
  2.与端口绑定(bind)
  3.对端口进行监听(listen)
  4.调用accept阻塞,等待客户端连接
    
# 客户端
  1.初始化一个Socket
  2.连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了

# 传输数据
  1.客户端发送数据请求
  2.服务器端接收请求并处理请求,然后把回应数据发送给客户端
  3.客户端读取数据
  4.最后关闭连接,一次交互结束

四 socket模块函数用法

import socket

# socket 初始化
socket.socket(socket_family,socket_type,protocal=0)
# 参数 
  socket_family: AF_UNIX 或 AF_INET # 指定套接字家族类型
  socket_type: 
    SOCK_STREAM # 流式协议 (tcp协议)  默认
    SOCK_DGRAM  # 数据报协议 (udp协议) 
  protocol: 一般不填,默认值为 0

# 获取tcp/ip套接字
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_sock = socket.socket()
 
# 获取udp/ip套接字
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  

4.1 服务端函数

s.bind()   # 绑定(主机,端口号)到套接字
s.listen() # 开始监听端口访问
s.accept() # 被动接受TCP客户的连接,(阻塞式)等待连接的到来

4.2 客户端函数

s.connect()    # 主动初始化向服务器连接
s.connect_ex() # connect()函数的扩展版本,出错时返回出错码, 而不是抛出异常

4.3 公共函数

s.recv()  # 接收tcp数据

s.send()  # 发送tcp数据
s.sendall()  # 发送完整的TCP数据 (本质就是循环调用send)

# 区别:
  send 在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完
  sendall 在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)

s.recvfrom() # 接收UDP数据
s.sendto()  # 发送UDP数据

s.getpeername() # 连接到当前套接字的远端的地址
s.getsockname() # 当前套接字的地址

s.getsockopt() # 返回指定套接字的参数
s.setsockopt() # 设置指定套接字的参数

s.close() # 关闭套接字ss

4.4 面向锁的函数

s.setblocking() # 设置套接字的阻塞与非阻塞模式
s.settimeout()  # 设置阻塞套接字操作的超时时间
s.gettimeout()  # 得到阻塞套接字操作的超时时间

4.5 面向文件的函数

s.fileno()   # 套接字的文件描述符
s.makefile() # 创建一个与该套接字相关的文件

五 socket通信案例

5.1 基于TCP的套接字

tcp是基于链接的,必须先启动服务端,然后再启动客户端去链接服务端

5.1.1 简单套接字通信

###### 服务端
import socket

# 1.买手机
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 流式协议=》 tcp协议

# 2.绑定手机卡
server.bind(('127.0.0.1', 8080))  # 参数是元祖形式
  # 端口 0-65535,1024之前都被系统保留使用

# 3.开机
server.listen(5)  # 5指的是半连接池的大小,可以直接 5

# 4.等待电话连接请求,拿到电话连接conn
conn, client_addr = server.accept()  # 会产生一个元祖,包含一个连接对象和客户端的IP端口地址

# 5.进行通话通信,收发消息
data = conn.recv(1024)  # 一次接受的最大数据量为1024 Bytes,收到的是bytes类型
print('客户端发来的消息:', data.decode('utf-8'))

conn.send(data.upper())

# 6.关闭电话连接(必选的回收资源的操作)
conn.close()

# 7. 关机(可选操作,通常不会关闭) 服务器关闭
server.close()  # 有时候关闭后,端口还被占用,是因为这一步是操作系统去执行端口释放,可能会有延迟。


###### 客户端
import socket

# 1.买手机
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)

# 2.拨通电话连接请求
client.connect(('127.0.0.1', 8080))

# 3.通信,收发消息
client.send('hello edmond hahaha'.encode('utf-8'))  # 发送必须是bytes类型
# client.send(b'hello edmond hahaha')

data = client.recv(1024)
print(data.decode('utf-8'))

# 4. 关闭连接(必选的回收资源的操作)
client.close()


# 注:
  客户端全是由socket 对象 client来调用
  服务端 有连接accept对象 和socket 对象的操作

5.1.2 添加链接与通信循环的通信

###### 服务端
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 流式协议=》 tcp协议
server.bind(('127.0.0.1', 8080))
server.listen(5)

while True:  # 链接循环 实际应该是多线程来链接
    conn, client_addr = server.accept()  

    while True:  # 通信循环
        # 针对Windows系统,客户端非法断开,会抛出异常,故采用异常处理方法,断开连接
        try:
            data = conn.recv(1024)  
            """
            # 在Linux系统中,一旦data收到的是空,就意味着是一种异常的行为:客户端非法断开链接了
            if len(data) == 0 :
                break
            """
            if data.decode('utf-8') == 'quit': break
            print('客户端发来的消息:', data.decode('utf-8'))
            conn.send(data.upper())
        except Exception:
            break
    conn.close()


###### 客户端
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 len(msg) == 0 :continue
    # 注意 这里请求的quit 要先发送到服务器端,再两边分别判断断开
    client.send(msg.encode('utf-8'))
    if msg == 'quit': break
    data = client.recv(1024)
    print(data.decode('utf-8'))
client.close()

5.1.3 报错解决:端口占用

# 报错:
  在重启服务端时可能会遇到: [Error 48] Address already in use
        
# 原因:
  由于你的服务端仍然存在四次挥手的time_wait状态,在占用端口地址
    
    
# 解决办法

# 方式1:在监听端口前,加入一条socket配置  重用ip和端口
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) # 重用ip和端口
server.bind(('127.0.0.1',8080))

# 方式2:通过调整linux内核参数解决
  发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决
    
# 1.编辑文件,加入以下内容
vi /etc/sysctl.conf

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

# 2.执行命令 让参数生效
/sbin/sysctl -p 


# 参数解读:
tcp_syncookies = 1 # 表示开启SYN Cookies  默认为0,表示关闭
  当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击

tcp_tw_reuse = 1  # 表示开启重用  默认为0,表示关闭
  允许将TIME-WAIT sockets重新用于新的TCP连接

tcp_tw_recycle = 1 # 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭

tcp_fin_timeout = 30 # 修改系統默认的 TIMEOUT 时间

5.2 基于UDP的套接字

udp是无链接的,先启动哪一端都不会报错

###### 服务端

# 注意:
    1.udp协议 sendto 与 recvfrom 一定是 一一对应的,不然数据会丢失
    2.虽然 先启动客户端与服务端 都没有问题,但是如果先启动客户端的话,发送数据到局域网,
      因为没有服务端接受,数据就会被丢掉。所以,一般还是先启动服务端
    3.udp协议 不出现粘包问题,因为传输时是数据报形式,每段数据是加上了一个报头,是有边界的,每次传送的数据都是完整的。
    4.若接收端的缓冲池大小 小于 发送的数据大小时,接收端会出现 只接受到 部分数据,还有部分数据会丢失。
    5. udp协议 通常 是传送小文件的,太大的话,不稳定,一般是512字节。


from socket import *

server = socket(AF_INET, SOCK_DGRAM)  # 数据报协议====》udp协议
server.bind(('127.0.0.1', 8080))

while True:
    ask_data, client_addr = server.recvfrom(1024)
    print('客户端说:', ask_data.decode('utf-8'))
    
    recv_data = input('服务端说:')
    server.sendto(recv_data.encode('utf-8'), client_addr)
server.close()


###### 客户端
from socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
    ask_data = input('客户端说:')
    client.sendto(ask_data.encode('utf-8'), ('127.0.0.1', 8080))
    recv, server_addr = client.recvfrom(1024)  # 是一个元祖,包含数据和收到的IP地址
    print('服务器说:', recv.decode('utf-8'))
client.close()

六 粘包问题

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

# 粘包问题
  主要因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

# udp协议 不出现粘包问题
  因为传输时是数据报形式,每段数据是加上了一个报头,是有边界的,每次传送的数据都是完整的。

6.1 粘包问题介绍

两台电脑在进行收发数据时,其实不是直接将数据传输给对方
  对于发送者: 执行 sendall/send 发送消息时,是将数据先发送至自己网卡的 写缓冲区
            再由缓冲区将数据发送给到对方网卡的读缓冲区
            
  对于接受者: 执行 recv 接收消息时,是从自己网卡的读缓冲区获取数据

所以,如果发送者连续快速的发送了2条信息,接收者在读取时会认为这是1条信息,即:2个数据包粘在了一起。


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

# 解决核心法门就是:每次都收干净,不要任何残留

6.2 粘包解决

6.2.1 struct 模块

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

struct.pack('i',1111111111111)  # 4位的bytes

6.2.2 固定模板

# 解决粘包问题最终版思路:(固定模板)  struct+json

###### 一、发送端总体思路:
  先定义头;将头转成json字符串;再将头的长度打包;依次发送头的长度、头信息、真实数据

# 1.拿到需要发送数据的总大小
total_size = len(stderr_res)+len(stdout_res)

# 2. 定义头 为字典,包含头的固定长度和其他描述信息,包括数据的总大小 total_size = xxxx等
header_dic = {
    'filename': '远程命令的结果',
    'total_size': 555,
    'else_inf': '其他信息'}

# 3.将字典头 转成json 字符串,并进行编码为可以发送的 二进制 字符串
json_str = json.dumps(header_dic)
json_str_bytes = json_str.encode('utf-8')

# 4.取到转化json 字符串后的头长度大小,并利用 struct 模块,将头的长度大小,打包成固定大小的Bytes 类型
x = struct.pack('i', len(json_str_bytes))

# 5.再将头的长度信息,发送过去
conn.send(x)

# 6.再将头的信息,发送过去
conn.send(json_str_bytes)

# 7.最后 发送真实数据信息


###### 二、接收端总体思路
  先接受到头,并把数据的总大小 total_size 解压出来
    
# 1.先接受头 (先收4个字节,从中提取接下来要收的头的长度)
x = client.recv(4)

# 2.利用 struct.unpack(),将头的长度 解压出来,
head_len = struct.unpack('i', x)[0]    # 解压出来是一个元祖:(x,)

# 3.根据头的长度,将头信息由json 转成原python类型:字典,并打印头
json_str_bytes = client.recv(head_len)
json_str = json_str_bytes.decode('utf-8')  
# 这两步可以放一起: json_str = client.recv(head_len).decode('utf-8')

header_dic = json.loads(json_str)
print(header_dic)

# 4.把字典中 key为 total_size的值 取出来;
total_size = header_dic.get('total_size')

# 5.最后 根据total_size,循环接受真实的数据
recv_size = 0
while recv_size < total_size:
  recv_data = client.recv(1024)
  recv_size += len(recv_data)
  print(recv_data.decode('gbk'), end='')

6.3 粘包案例

  • 服务端

    import struct
    import subprocess
    import json
    from socket import *
    
    server = socket(AF_INET, SOCK_STREAM)
    server.bind(('127.0.0.1', 8080))
    server.listen(5)
    
    # 链接循环
    while True:
        conn, client_addr = server.accept()
    
        # 通信循环
        while True:
            try:
                cmd = conn.recv(1024)
                if len(cmd) == 0:break
                print('操作的命令:', cmd.decode('utf-8'))
                obj = subprocess.Popen(cmd.decode('utf-8'),
                                       shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE
                                       )
                stdout_res = obj.stdout.read()
                stderr_res = obj.stderr.read()
    
                # 1.拿到需要发送数据的总大小
                total_size = len(stderr_res)+len(stdout_res)
    
                # 2. 定义头 为字典,包含头的固定长度和其他描述信息,包括数据的总大小 total_size = xxxx等
                head_dic = {
                    'filename': '远程命令的结果',
                    'total_size': total_size,
                    'else_inf': '其他信息'
                }
    
                # 3.将字典头 转成json 字符串,并进行编码为可以发送的 二进制 字符串
                head = json.dumps(head_dic).encode('utf-8')
    
                # 4.取到转化json 字符串后的头长度大小,并利用 struct 模块,将头的长度大小,打包成固定大小的Bytes 类型
                x = len(head)
                header = struct.pack('i', x)
    
                # 5.再将头的长度信息,发送过去
                conn.send(header)
    
                # 6.再将头的信息,发送过去
                conn.send(head)
    
                # 7.最后,发送真实数据信息
                conn.send(stdout_res)
                conn.send(stderr_res)
    
            except Exception:
                break
    
        conn.close()
    
  • 客户端

    import struct
    import json
    from socket import *
    
    client = socket(AF_INET, SOCK_STREAM)
    client.connect(('127.0.0.1', 8080))
    
    # 通信循环
    while True:
        cmd = input('请输入操作指令:').strip()
        if len(cmd) == 0: continue
        client.send(cmd.encode('utf-8'))
    
        # 1.先接受到头的长度
        header = client.recv(4)
    
        # 2.利用 struct.unpack(),将头的长度 解压出来,
        x = struct.unpack('i', header)[0]    # 解压出来是一个元祖:(x,)
    
        # 3.根据这个长度,将头信息由json 转成原python类型:字典
        head = client.recv(x).decode('utf-8')
        head_dic = json.loads(head)
    
        # 测试打印下字典头
        print(head_dic)
    
        # 4.把字典中 key为 total_size的值 取出来;
        total_size = head_dic.get('total_size')
    
        # 5.最后 根据total_size,循环接受真实的数据
        recv_size = 0
        while recv_size < total_size:
            recv_data = client.recv(1024)  # 本次接受,最大接受为1024 Bytes
            recv_size += len(recv_data)
            print(recv_data.decode('gbk'), end='')
    
        print()
    
    client.close()
    

七 socketserver实现并发

# socketserver模块中分两大类
  server类   # 解决链接问题
  request类  # 解决通信问题

# 并发:
  IO密集   多线程
  计算密集  多进程

7.1 基于TCP实现并发

服务端

import socketserver

# 基于tcp的socketserver我们自己定义的类中的
  self.server   # 套接字对象
  self.request  # 一个链接,tcp是 这个链接收发数据,而udp是没有链接,是套接字对象收发数据
  self.client_address  # 客户端地址

# 继承 BaseRequestHandler类  写通信逻辑
class MyRequestHandle(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)  # 如果是tcp协议,self.request====>conn连接对象
        print(self.client_address)  # self.client_address====>conn连接对象的IP和端口
        while True:
            try:
                msg = self.request.recv(1024)
                if msg == 0:break
                print('客户端发来的消息:', msg.decode('utf-8'))
                self.request.send(msg.upper())
            except Exception:
                break
        self.request.close()

# 使用 ThreadingTCPServer类 开启多线程链接循环
server_obj = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyRequestHandle)

server_obj.serve_forever()
# 就等同于链接循环,并启动一个线程,把链接对象 conn 和 客户端地址信息 client.address 传递过去

客户端

import socket

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

while True:
    msg = input('>>>请输入:').strip()
    client.send(msg.encode('utf-8'))
    data = client.recv(1024)
    print(data.decode('utf-8'))

7.2 基于UDP实现并发

服务端

import socketserver
import time

# 基于udp的socketserver我们自己定义的类中的
  self.request  # 是一个元组 (客户端发来的数据,服务端的udp套接字对象)
  self.client_address  # 即客户端地址

# 继承 BaseRequestHandler类  写通信逻辑
class MyRequestHandle(socketserver.BaseRequestHandler):
    def handle(self):
        client_data = self.request[0]
        server = self.request[1]
        print('客户端:{}发来的数据:{}'.format(self.client_address, client_data))
        server.sendto(client_data.upper(), self.client_address)
        time.sleep(10)

# 使用 ThreadingUDPServer类 开启多线程链接循环
server_obj = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyRequestHandle)
server_obj.serve_forever()

客户端

from socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
    msg = input('>>>请输入:').strip()
    if msg == 'q':
        break
    client.sendto(msg.encode('utf-8'), ('127.0.0.1', 8080))
    recv, server_addr = client.recvfrom(1024)
    print(recv.decode('utf-8'))

client.close()
posted @ 2022-07-06 14:29  Edmond辉仔  阅读(27)  评论(0编辑  收藏  举报