Socket - 套接字编程

2022-03-25 19:06:33 浏览数 (1)

目录

  • Socket - 套接字编程
    • Socket 通信流程图
    • Socket模块
      • socket()方法
        • 部分参数
      • bind()方法
      • listen()方法
      • accept()方法
        • accept()函数返回值:sock、addr
      • connect() 方法
      • close()方法
      • 服务端客户端对比
      • 简单案例
    • 简易通信循环
      • 优化通信循环
    • 黏包问题
    • Struct 模块
      • 规定报头
      • 解决黏包问题
    • 上传文件案例
    • UDP通信

Socket - 套接字编程

只要涉及到远程数据交互必须要操作OSI七层模型,那么每层都需要相应的程序去操作,现在就需要一个模块去操作,直接实现; Socket是处于应用层和传输层之间的抽象层,Socket并不是一个单独的层,在我们设计程序软件的时候,它会让编程变的更简单,我们大量用的都是通过socket实现的; Socket的作用显而易见,TCP和UDP比喻成小弟,socket是大哥,那么下面的协议(TCP/UDP)不需要我们去管,这样暴露出来的只有Socket接口,Socket自动的去组织数据,来符合指定的协议标准;

Socket 通信流程图

Socket基于TCP/IP协议的面向连接的通信,分为客户端和服务端必须先启动服务端,然后再启动客户端去链接服务端

Socket模块

socket()方法

客户端和服务端的入口,默认就是基于网络的TCP协议传输;

部分参数

套接字家族:

  • AF_UNIX:本机通信
  • AF_INET:TCP/IP协议,使用IPV4,基于网络传输
  • AF_INET6:TCP/IP协议,使用IPV6,基于网络传输

类型分类(type)

  • SOCK_STREAM:TCP协议(默认采用,流式协议)
  • SOCK_DGRAM:UDP协议
  • SOCK_RAW:原始套接字

proto参数是协议标志,默认为0,原始套接字需要指定值

部分源码

bind()方法

绑定函数的作用就是为调用socket()函数产生的套接字分配一个本地协议地址,建立地址与套接字的对于关系;

代码语言:javascript复制
# 源码
def bind(self, address: Union[_Address, bytes]) -> None: ...
# 参数
address: Union[_Address, bytes]
address:要接收的数据类型集合
->:返回什么(返回值)
# 示例bind((ip,端口))
server.bind(('127.0.0.1', 8080))  # 绑定ip和端口

listen()方法

监听函数,作用是建立半连接池,规定最大连接数,在windows系统下如果客户端数量超过半连接池规定的数量会报错;

代码语言:javascript复制
server.listen(5)  # 半连接池  
# 如果服务端正在和一个客户端做交互,那么半连接池就规定了,还可以服务几个客服端;
# 类似于,餐厅门口可以让顾客坐的凳子,满了就不能坐了

accept()方法

作用就是使服务器接受客户端的连接请求; 运行服务端,会在此监听,等待请求;

代码语言:javascript复制
def accept(self) -> Tuple[socket, _RetAddress]: ...
# 返回一个元组,元组包括socket对象和地址信息
"""
accept() -> (socket object, address info)

        Wait for an incoming connection.  Return a new socket
        representing the connection, and the address of the client.
        For IP sockets, the address info is a pair (hostaddr, port).
        """
accept()函数返回值:sock、addr
  • sock:用于操作服务端和客户端连接的双向通道的媒介
  • addr:客户端的地址
  • sock.recv():接收消息,返回bytes类型数据
代码语言:javascript复制
def recv(self, bufsize: int, flags: int = ...) -> bytes: ...
# 示例
sock.recv(1024) # 接收客户端发送的消息,一次接收1024bytes
  • sock.send():发送消息,返回int类型数据
代码语言:javascript复制
def send(self, data: bytes, flags: int = ...) -> int: ...
# 示例
sock.send(b'HammerZe')

connect() 方法

作用是TCP客户端连接服务器

代码语言:javascript复制
def connect(self, address: Union[_Address, bytes]) -> None: ...

# 示例
# 格式:connect((ip,port)),里面是tuple类型
client.connect(('127.0.0.1', 8080))

close()方法

关闭套接字,并立即返回到进程;

代码语言:javascript复制
sock.close()  
server.close() 

服务端客户端对比

编写小妙招:

服务端

客户端

对比

server=socket.socket()

client=socket.socket()

开头必须

server.bind((ip,port))

client.connect((ip,port))

连接必须

server.listen(n) # 半连接池

服务端

sock,addr = server.accept() # 获取客户端请求,返回sock,addr(客户端地址)

服务端

sock.recv(1024) # 服务端接收内容,1024为size

client.send(bytes)# 客户端发送bytes类型数据

服务端和客户端相对

sock.send(bytes)# 服务端发送bytes类型数据

client.recv(1024)# 客户端接收内容,1024为size

服务端和客户端相对

sock.close() # 关闭会话通道,断开连接

服务端

server.close()# 关闭套接字

client.close()# 关闭套接字

服务端和客户端

注意:服务端和客户端不可同时发数据(send),也不可同时收数据(recv)

简单案例

服务端

代码语言:javascript复制
'''server.py'''
import socket

server = socket.socket()
# 建立连接
server.bind(('127.0.0.1', 8085))
# 半连接池
server.listen(5)
# 获取客户端请求
sock, address = server.accept()
print(address)
# 接收数据
data = sock.recv(1024)
print(data)
# 发送数据
sock.send(b"Hi,This is server-side!")
# 关闭通话
sock.close()
# 关闭套接字
server.close()

客户端

代码语言:javascript复制
'''client.py'''
import socket

client = socket.socket()
# 建立连接
client.connect(('127.0.0.1', 8085))
# 发送数据
client.send(b"Hello guys,I'am HammerZe!")
# 接收数据
data = client.recv(1024)
print(data)
# 关闭套接字
client.close()

简易通信循环

光发一条消息不够过瘾是吧,如何通信循环,你侬我侬,如下:

服务端

代码语言:javascript复制
import socket

server = socket.socket()
# 建立链接
server.bind(('127.1.1.1', 8086))
# 半连接池
server.listen(5)
# 监听
sock,address = server.accept()

while True:
    # 接收
    data = sock.recv(1024)
    print(data.decode('utf8'))
    # 发送
    to_cmsg = input('请输入发给客户端的内容>>>:').strip()
    sock.send(to_cmsg.encode('utf8'))

客户端

代码语言:javascript复制
import socket

client = socket.socket()
# 获取ip和端口
client.connect(('127.1.1.1', 8086))
# 发送数据
while True:
    to_smsg = input('请输入发给服务端的内容>>>:').strip()
    client.send(to_smsg.encode('utf8'))
    # 接收数据
    data = client.recv(1024)
    print(data.decode('utf8'))

优化通信循环

优化内容:

  • 解决,发空消息会停住,导致双方接收和发送混乱
  • 解决Address already in use 报错(端口占用错误)
  • 解决服务端启用,客户端主动重启报错,错误内容:远程主机强迫关闭了一个现有的连接,原因是没有走三次握手和四次挥手主动断开;

服务端

代码语言:javascript复制
import socket
from socket import SOL_SOCKET,SO_REUSEADDR


server = socket.socket()
# 解决断开占用报错
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
# 建立链接
server.bind(('127.1.1.1', 8086))
# 半连接池
server.listen(5)
# 监听
while True:
    sock,address = server.accept()

    while True:
        try:
            # 接收
            data = sock.recv(1024)
            if len(data) ==0:continue
            print(data.decode('utf8'))
            to_cmsg = data  b'server'
            sock.send(to_cmsg)
        except Exception:
            break

客户端

代码语言:javascript复制
import socket

client = socket.socket()
# 获取ip和端口
client.connect(('127.1.1.1', 8086))
# 发送数据
while True:
    to_smsg = input('请输入发给服务端的内容>>>:').strip()
    if not to_smsg:
        print('不能输入空消息,请重新输入')
        continue
    client.send(to_smsg.encode('utf8'))
    # 接收数据
    data = client.recv(1024)
    print(data.decode('utf8'))

黏包问题

数据管道的数据没有被完全取出;TCP特性导致黏包,当数据量比较小 且时间间隔比较短,交互多次数据,那么TCP会自动打包成一个数据包发送; 情景一:如果交互的数据比规定接收的字节大,那么只会接收规定的字节大小,那么下次通信,继续传输上次没有传完的数据(互通管道,先进先出,TCP流式协议); 情景二:如果交互的数据太小,比如想交互三次发三次hello,那么TCP会一次发完;

  • 解决办法:调整规定接收size,调大或调小(不推荐)
  • 使用Struct规定固定报头(推荐)

Struct 模块

使用Struct模块规定了报头的长度,通过服务端定制报头和客户端解析报头来获取真实数据的长度,从而接收真实的数据内容,解决黏包问题;

规定报头

案例

代码语言:javascript复制
import struct
import json

info_dic = {
    'name':'Hammer',
    'age':18,
}
# 序列化
json_info_dic = json.dumps(info_dic)
# 真实长度
print(len(json_info_dic))  # 29

# 定制报头
herder = struct.pack('i',len(json_info_dic))
print(len(herder))  # 4
# 解析报头
parse_herder = struct.unpack('i',herder)[0]
print(parse_herder)  # 29

解决黏包问题

客户端

代码语言:javascript复制
import socket
import subprocess
import json
import struct

server = socket.socket()
server.bind(('127.1.1.2', 8087))
server.listen(5)

while True:
    sock, address = server.accept()
    while True:
        data = sock.recv(1024)  # 接收cmd命令
        command_cmd = data.decode('utf8')
        sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        res = sub.stdout.read()   sub.stderr.read()  # 结果可能很大
        # 1.制作报头
        data_first = struct.pack('i', len(res))
        # 2.发送报头
        sock.send(data_first)
        # 3.发送真实数据
        sock.send(res)

服务端

代码语言:javascript复制
import socket
import struct

client = socket.socket()  
client.connect(('127.1.1.2', 8087))  

while True:
    msg = input('请输入cmd命令>>>:').strip()
    if len(msg) == 0:
        continue
    client.send(msg.encode('utf8'))
    # 1.先接收固定长度为4的报头数据
    recv_first = client.recv(4)
    # 2.解析报头
    real_length = struct.unpack('i',recv_first)[0]
    # 3.接收真实数据
    real_data = client.recv(real_length)
    print(real_data.decode('gbk'))

上传文件案例

客户端

代码语言:javascript复制
import json
import os
import socket
import struct

client = socket.socket()
client.connect(('127.0.0.1', 8080))

while True:
    data_path = r'C:Users32972Desktoppython课外题目'
    # print(os.listdir(data_path)) # ['python练习题.md', 'Python阶段面试题2019-2-15.docx']
    file_list = os.listdir(data_path)
    for index, value in enumerate(file_list, 1):
        print(index, value)  # 0 python练习题.md   1 Python阶段面试题2019-2-15.docx

    file_choice = input('请输入想要上传的文件编号>>>>').strip()

    if not file_choice:
        print('上传文件编号不能为空')
        continue
    if not file_choice.isdigit():
        print('文件编号必须是纯数字')
        continue
    file_choice = int(file_choice)
    if file_choice not in range(1, len(file_list)   1):
        print('没有该文件')
        continue
        # 获取文件名称
    file_name = file_list[file_choice - 1]
    print(file_name)
    # 拼接路径
    real_file_path = os.path.join(data_path, file_name)
    # print(real_file_path)  # C:Users32972Desktoppython课外题目python练习题.md
    # 优化操作
    data_dict = {
        'file_name': file_name,
        'desc': 'python面试题',
        'size': os.path.getsize(real_file_path),
        'info': '通关面试必备!'
    }
    data_json = json.dumps(data_dict)
    # 制作字典报头
    data_first = struct.pack('i', len(data_json))
    # 发送字典报头
    client.send(data_first)
    # 发送字典
    client.send(data_json.encode('utf8'))
    # 发送真实数据
    with open(real_file_path, 'rb') as f:
        for line in f:
            client.send(line)

服务端

代码语言:javascript复制
import json
import socket
import struct


server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen(5)


while True:
    sock,address = server.accept()
    while True:
        # 接收报头
        recv_first = sock.recv(4)
        # 解析字典报头
        dict_length = struct.unpack('i',recv_first)[0]
        # 接收字典数据
        real_data = sock.recv(dict_length)
        # 解析字典,返序列化
        real_dict = json.loads(real_data)
        print(real_dict)
        # 获取字典中的内容
        data_size = real_dict.get('size')
        file_name = real_dict.get('file_name')


        # 接收
        recv_size = 0
        with open(file_name,'wb') as f:
            while recv_size<data_size:
                data = sock.recv(1024)
                recv_size  = len(data)
                f.write(data)

UDP通信

代码语言:javascript复制
import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)  # UDP协议
udp_sk.bind(('127.0.0.1',9000))  # 绑定地址
msg,addr = udp_sk.recvfrom(1024)
udp_sk.sendto(b'hi',addr)             
udp_sk.close() 


import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

0 人点赞