网络编程之基于tcp协议和udp协议的套接字通信

一、套接字socket

  1、在任何类型的通信开始之前,网络应用程序都必须创建套接字。

     2、socket一般指套接字,套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC),随着socket的不断更新。现在可以实现不同主机之间进行通信的工具。

  3、有两种类型的套接字:基于文件的和面向网络的

    基于文件的套接字

家族名:AF_UNIX,(又名AF_LOCAL,在POSIX1.g标准中指定),它代表地址家族(addressfamily):UNIX。
其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocolfamily),并使用其缩写PF而非AF。
类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX

    基于网络的套接字

家族名:AF_INET,或者地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。
此外,还有其他的地址家族,在所有的地址家族之中,目前AF_INET是使用得最广泛的

 

  4、面向连接的套接字与无连接套接字

    面向连接的套接字:TCP套接字的名字SOCK_STREAM,特点:可靠,开销大

    无连接套接字:UDP套接字的名字SOCK_DGRAM,特点:不可靠(局网内还是比较可靠的),开销小

 

二、基于tcp协议的套接字通信

简单版:

1、客户端

import socket
 
#1、整个手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议
 
#2、拨通服务端电话
phone.connect(('127.0.0.1',8081))
 
#3、通信
import time
time.sleep(10)
phone.send('hello egon 哈哈哈'.encode('utf-8'))    #发送数据
data=phone.recv(1024)          #接收服务端返回数据
print(data.decode('utf-8'))
 
#4、关闭连接(必选的回收资源的操作)
phone.close()

2、服务端

import socket
 
# 1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议
 
# 2、绑定手机卡
phone.bind(('127.0.0.1',8081)) # 0-65535, 1024以前的都被系统保留使用
 
# 3、开机
phone.listen(5) # 5指的是半连接池的大小
print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080))
# 4、等待电话连接请求:拿到电话连接conn
conn,client_addr=phone.accept()
# print(conn)
print("客户端的ip和端口:",client_addr)
 
# 5、通信:收\发消息
data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型
print("客户端发来的消息:",data.decode('utf-8'))
conn.send(data.upper())
 
# 6、关闭电话连接conn(必选的回收资源的操作)
conn.close()
 
# 7、关机(可选操作)
phone.close()

 

加上通信循环版:

1、客户端

import socket
 
#1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议
 
#2、拨通服务端电话
phone.connect(('127.0.0.1',8083))
 
#3、通信
while True:
    msg=input("输入要发送的消息>>>: ").strip() #msg=''
    if len(msg) == 0:continue
    phone.send(msg.encode('utf-8'))
    print('======?')
    data=phone.recv(1024)
    print(data.decode('utf-8'))
 
#4、关闭连接(必选的回收资源的操作)
phone.close()

2、服务端

import socket
 
# 1、买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 流式协议=》tcp协议
 
# 2、绑定手机卡
phone.bind(('127.0.0.1',8083)) # 0-65535, 1024以前的都被系统保留使用
 
# 3、开机
phone.listen(5) # 5指的是半连接池的大小
print('服务端启动完成,监听地址为:%s:%s' %('127.0.0.1',8080))
 
# 4、等待电话连接请求:拿到电话连接conn
conn,client_addr=phone.accept()
 
# 5、通信:收\发消息
while True:
    try:
        data=conn.recv(1024) # 最大接收的数据量为1024Bytes,收到的是bytes类型
        if len(data) == 0:
            # 在unix系统洗,一旦data收到的是空
            # 意味着是一种异常的行为:客户度非法断开了链接
            break
        print("客户端发来的消息:",data.decode('utf-8'))
        conn.send(data.upper())
    except Exception:
        # 针对windows系统
        break
 
# 6、关闭电话连接conn(必选的回收资源的操作)
conn.close()
 
# 7、关机(可选操作)
phone.close()

 

加上链接循环:同上,打开多个服务端,一个客户端来实现

 

基于tcp协议实现远程执行命令

  粘包问题:当send一条信息时,无论底层怎样分段分片,TCP协议层会把构成整条信息的数据段排序完成后才呈现在内核缓冲区。即面向流的通信是无消息保护边界的。所以粘包问题是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。只有tcp有粘包问题,而udp没有粘包问题

1、客户端

from socket import *
 
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8082))
 
while True:
    cmd=input('请输入命令>>:').strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode('utf-8'))
 
    # 解决粘包问题思路:
    # 1、拿到数据的总大小total_size
    # 2、recv_size=0,循环接收,每接收一次,recv_size+=接收的长度
    # 3、直到recv_size=total_size,结束循环
    cmd_res=client.recv(1024) # 本次接收,最大接收1024Bytes
    print(cmd_res.decode('utf-8')) # 强调:windows系统用gbk
 
 
# 粘包问题出现的原因
# 1、tcp是流式协议,数据像水流一样粘在一起,没有任何边界区分
# 2、收数据没收干净,有残留,就会下一次结果混淆在一起
 
# 解决的核心法门就是:每次都收干净,不要任何残留

2、服务端

# 服务端应该满足两个特点:
# 1、一直对外提供服务
# 2、并发地服务多个客户端
import subprocess
from socket import *
 
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加#在绑定前调用setsockopt 让套接字允许地址重用
server.bind(('127.0.0.1',8082))
server.listen(5)
 
#  服务端应该做两件事
# 第一件事:循环地从板连接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
    conn,client_addr=server.accept()
 
    # 第二件事:拿到链接对象,与其进行通信循环
    while True:
        try:
            cmd=conn.recv(1024)
            if len(cmd) == 0:break
            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()
            print(len(stdout_res)+len(stderr_res))
            # conn.send(stdout_res+stderr_res) # ???
            conn.send(stdout_res)
            conn.send(stderr_res)
 
            # with open("1.mp4",mode='rb') as f:
            #     for line in f:
            #         conn.send(line)
        except Exception:
            break
    conn.close()

  

  粘包问题的解决

    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是发送端在发送数据前,发一个头文件包,告诉发送的字节流总大小,然后接收端来一个死循环接收完所有数据

    使用struct模块可以用于将Python的值根据格式符,转换为字符串(byte类型)

    struct模块中最重要的三个函数是pack(), unpack(), calcsize()

      pack(fmt, v1, v2, ...)     按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)

      unpack(fmt, string)       按照给定的格式(fmt)解析字节流string,返回解析出来的tuple

      calcsize(fmt)                 计算给定的格式(fmt)占用多少字节的内存

1、客户端的解决

import struct
import json
from socket import *
 
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8083))
 
while True:
    cmd=input('请输入命令>>:').strip()
    if len(cmd) == 0:continue
    client.send(cmd.encode('utf-8'))
 
    # 接收端
    # 1、先手4个字节,从中提取接下来要收的头的长度
    x=client.recv(4)
    header_len=struct.unpack('i',x)[0]
 
    # 2、接收头,并解析
    json_str_bytes=client.recv(header_len)
    json_str=json_str_bytes.decode('utf-8')
    header_dic=json.loads(json_str)
    print(header_dic)
    total_size=header_dic["total_size"]
 
    # 3、接收真实的数据
    recv_size = 0
    while recv_size < total_size:
        recv_data=client.recv(1024)
        recv_size+=len(recv_data)
        print(recv_data.decode('utf-8'),end='')
    else:
        print()
 

2、服务端解决

import subprocess
import struct
import json
from socket import *
 
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
server.bind(('127.0.0.1',8083))
server.listen(5)
 
#  服务端应该做两件事
# 第一件事:循环地从板连接池中取出链接请求与其建立双向链接,拿到链接对象
while True:
    conn,client_addr=server.accept()
 
    # 第二件事:拿到链接对象,与其进行通信循环
    while True:
        try:
            cmd=conn.recv(1024)
            if len(cmd) == 0:break
            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()
            total_size=len(stdout_res)+len(stderr_res)
 
            # 1、制作头
            header_dic={
                "filename":"a.txt",
                "total_size":total_size,
                "md5":"123123xi12ix12"
            }
 
            json_str = json.dumps(header_dic)
            json_str_bytes = json_str.encode('utf-8')
 
 
            # 2、先把头的长度发过去
            x=struct.pack('i',len(json_str_bytes))
            conn.send(x)
 
            # 3、发头信息
            conn.send(json_str_bytes)
            # 4、再发真实的数据
            conn.send(stdout_res)
            conn.send(stderr_res)
 
        except Exception:
            break
    conn.close()

 

三、基于udp协议的套接字通信

1、客户端

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',8081))
    res=client.recvfrom(1024)
    print(res)
 
client.close()

2、服务端

import socket
 
server=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) # 数据报协议=》udp协议
 
server.bind(('127.0.0.1',8081))
 
while True:
    data,client_addr=server.recvfrom(1024)
    server.sendto(data.upper(),client_addr)
 
 
server.close()

 

udp协议没有粘包问题:

1、客户端

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

2、服务端

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

 

四、socket 之 send 和 recv 原理剖析

1、TCP socket 的发送和接收缓冲区

  当创建一个 TCP socket 对象的时候会有一个发送缓冲区和一个接收缓冲区,这个发送和接收缓冲区指的就是内存中的一片空间。

2、 send 原理剖析

send不是直接把数据发给服务端,要想发数据,必须得通过网卡发送数据,应用程序是无法直接通过网卡发送数据的,它需要调用操作系统接口,也就是说,应用程序把发送的数据先写入到发送缓冲区 (内存中的一片空间),再由操作系统控制网卡把发送缓冲区的数据发送给服务端网卡 。

3、 recv 原理剖析

recv 不是直接从客户端接收收数据,应用软件是无法直接通过网卡接收数据的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区 (内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据。

4、 send 和 recv 原理剖析图

 说明:

  1. 发送数据是发送到发送缓冲区

  2. 接收数据是从接收缓冲区获取

 

五、socketserver模块的简单使用

   Socketserver内部使用IO多路复用以及“多线程”和“多进程”,从而实现并发处理多个客户端请求的socket服务端。 即,每个客服端请求连接到服务器时,socket服务端都会在服务器上创建一个“线程”或“进程”专门负责处理当前客户端的所有请求。

  ThreadingTCPServer实现的socket服务器内部会为每个client创建一个“线程”,该线程用来和客户端就行交互 。

比如服务端代码

  

import socketserver
  
class MyRequestHandle(socketserver.BaseRequestHandler):          ## 必须继承BaseRequestHandler
    def handle(self):                                  #必须有handle方法
        # 如果tcp协议,self.request=>conn
        print(self.client_address)
        while True:
            try:
                msg = self.request.recv(1024)
                if len(msg) == 0: break
                self.request.send(msg.upper())
            except Exception:
                break
        self.request.close()
 
 
 
#  服务端应该做两件事
# 第一件事:循环地从半连接池中取出链接请求与其建立双向链接,拿到链接对象
s=socketserver.ThreadingTCPServer(('127.0.0.1',8889),MyRequestHandle)             #实例化对象,实现多线程的socket
s.serve_forever()       #事件监听,并调用MyRequestHandle方法
# 等同于 # while True: # conn,client_addr=server.accept() # 启动一个线程(conn,client_addr) # 第二件事:拿到链接对象,与其进行通信循环===>handle

服务端这样设置后,就能和多个客户端通信。

posted @ 2022-03-31 15:48  新入世界的小白  阅读(158)  评论(0编辑  收藏  举报