ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2022-07-06 14:32:40  阅读:160  来源: 互联网

标签:26 socket data 编程 server client 接字 recv


一 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()

标签:26,socket,data,编程,server,client,接字,recv
来源: https://www.cnblogs.com/Edmondhui/p/16450701.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有