image

一、socket套接字编程

  • socket是应用层与TCP/IP协议族的中间抽象层,它是一组接口,把复杂的TCP/IP协议族封装为几个简单的接口提供给应用层调用,实现程序在网络中的通信

  • socket仅仅是一个调用接口,为了方便程序员针对TCP或者UDP编程的接口。

    • 1、首先:要想开发一款自己的C/S架构软件,就必须掌握socket编程
    • 2、其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的
    • 3、然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。

img

socket层

从上图中,我们没有看到socket的影子,它在哪里呢?看下方图:

img

编写服务端简易步骤:

1、创建套接字

import socket
s1=socket.socket(family,type)
#family参数代表地址家族,可为AF_INET或AF_UNIX。AF_INET家族包括Internet地
址,AF_UNIX家族用于同一台机器上的进程间通信。
#type参数代表套接字类型,可为SOCK_STREAM(流套接字,就是TCP套接字)和
SOCK_DGRAM(数据报套接字,就是UDP套接字)。 
#默认为family=AF_INET  type=SOCK_STREM    
#返回一个整数描述符,用这个描述符来标识这个套接字

2、绑定套接字

s1.bind( address ) 
#由AF_INET所创建的套接字,address地址必须是一个双元素元组,格式是
(host,port)。host代表主机,port代表端口号。
#如果端口号正在使用、主机名不正确或端口已被保留,bind方法将引发socket.error
异常。 
#例: ('192.168.1.1',9999)

3、监听套接字

s1.listen( backlog ) 
#backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这
些请求需要排队,如果队列满,就拒绝请求。 

4、等待接受连接

connection, address = s1.accept()
#调用accept方法时,socket会时入“waiting”状态,也就是处于阻塞状态。客户请求
连接时,方法建立连接并返回服务器。
#accept方法返回一个含有两个元素的元组(connection,address)。
#第一个元素connection是所连接的客户端的socket对象(实际上是该对象的内存地
址),服务器必须通过它与客户端通信;
#第二个元素 address是客户的Internet地址。

5、处理阶段

connection.recv(bufsize[,flag])
#注意此处为connection
#接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag
提供有关消息的其他信息,通常可以忽略

connection.send(string[,flag])
#将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小
于string的字节大小。即:可能未将指定内容全部发送。

6、传输结束

s1.close()
#关闭套接字

编写客户端简易步骤:

1、创建socket对象

import socket
s2=socket.socket()

2、连接至服务器端

s2.connect(address)
#连接到address处的套接字。一般,address的格式为元组(hostname,port),如果
连接出错,返回socket.error错误。

3、处理阶段

s2.recv(bufsize[,flag])
#接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag
提供有关消息的其他信息,通常可以忽略

s2.send(string[,flag])
#将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小
于string的字节大小。即:可能未将指定内容全部发送。

4、连接结束,关闭套接字

s2.close()

image

简易代码组合:

socket模块
	
架构启动肯定是先启动服务端再启动客户端


# 服务端
import socket
"""
导入模块的两种方式
    import句式
    from...import...句式
第三方模块下载
    pip3 install 模块名==版本号 -i 仓库地址
"""
server = socket.socket()  # 默认就是基于网络的TCP传输协议   买手机
server.bind(('127.0.0.1', 8080))  # 绑定ip和port         插电话卡
server.listen(5)  # 半连接池            开机(过渡)
sock, address = server.accept()  # 监听   三次握手的listen态
print(address)  # 客户端地址
data = sock.recv(1024)  # 接收客户端发送的消息  听别人说话
print(data)
sock.send(b'hello my big baby~~~')  # 给别人回话
sock.close()  # 挂电话
server.close()  # 关机

# 客户端
import socket
client = socket.socket()  # 买手机
client.connect(('127.0.0.1', 8080))  # 拨号
# 说话
client.send(b'hello big DSB DSB DSB!')
# 听他说
data = client.recv(1024)
print(data)
client.close()

通信循环及代码优化,加异常处理

1.客户端校验消息不能为空
2.服务端添加兼容性代码(mac linux)
3.服务端重启频繁报端口占用错误
	from socket import SOL_SOCKET, SO_REUSEADDR
    server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)  # 在bind前加
4.客户端异常关闭服务端报错的问题
	异常捕获
5.服务端链接循环
6.半连接池
	设置可以等待的客户端数量

    
## 服务端.py文件

import socket
# 1、买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式协议
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)  关于端口
占用的修改

# 2、绑定电话卡
phone.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、开机
phone.listen(5)   # 半连接池 

# 4、接收链接请求 
while True:   # 链接循环
    conn,client_addr = phone.accept()
    print(client_addr)     
# 5、收发消息 
    while True:  # 通信循环
        try:  # 应对Windows系统的异常
            data = conn.recv(1024)
            if len(data) == 0:  # 应对linux系统的异常处理,正常情况data不会等于0
                break
            print(data)
            conn.send(data.upper()) 
	except Exception: # 应对Windows系统的异常
            break  # 无论应对哪个异常都应该将循环结束掉
# 6、挂电话
	conn.close()
# 4、关机
phone.close()


## 客户端.py文件

import socket
# 1、买手机
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  # 流式协议

# 2、打电话
phone.connect(('127.0.0.1',8080))  # 回环地址ip

# 3、发\收数据
while True:     # 加while循环
    msg = input('>>>>:').strip()
    if len(msg) == 0:   # 解决输入空的bug
        continue
    phone.send(msg.encode('utf-8'))  # 发消息
    data = phone.recv(1024)          # 收消息
    print(data.decode('utf-8'))  
# 4、关闭
phone.close()

'''
问题:关于端口占用的修改

在重启服务端时可能会遇到

OSError:[Errno 48] Address already in use  # 地址已经在使用中

这个是由于服务端仍然存在四次挥手的time_wait状态在占用地址
# 在绑定之前加入一条socket配置,重用ip和端口

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))

'''

黏包现象

  • 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
  • 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
数据管道的数据没有被完全取出
	
TCP协议有一个特性
	"""
	当数据量比较小 且时间间隔比较短的多次数据
	那么TCP会自动打包成一个数据包发送
	"""
    
报头
	能够标识即将到来的数据具体信息
    	eg:数据量多大 
    # 报头的长度必须是固定的

image

struct模块

  • python strtuct模块主要在Python中的值于C语言结构之间的转换。可用于处理存储在文件或网络连接(或其它来源)中的二进制数

import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt

#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fb
		f8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值

#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输

#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是
		报头的长度

#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式

#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度

head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头

#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

  • struct模块格式化对照表

img

简易版本报头

import socket
import subprocess
import json
import struct

server = socket.socket()
server.bind(('127.0.0.1', 8080))
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)
     
import socket
import struct

client = socket.socket()  # 买手机
client.connect(('127.0.0.1', 8080))  # 拨号

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

上传文件数据

import json
import socket
import struct
import os

client = socket.socket()  # 买手机
client.connect(('127.0.0.1', 8080))  # 拨号

while True:
    data_path = r'D:\金牌班级相关资料\网络并发day01\视频'
    # print(os.listdir(data_path))  # [文件名称1 文件名称2 ]
    movie_name_list = os.listdir(data_path)
    for i, j in enumerate(movie_name_list, 1):
        print(i, j)
    choice = input('请选择您想要上传的电影编号>>>:').strip()
    if choice.isdigit():
        choice = int(choice)
        if choice in range(1, len(movie_name_list) + 1):
            # 获取文件名称
            movie_name = movie_name_list[choice - 1]
            # 拼接文件绝对路径
            movie_path = os.path.join(data_path, movie_name)
            # 1.定义一个字典数据
            data_dict = {
                'file_name': 'XXX老师合集.mp4',
                'desc': '这是非常重要的数据',
                'size': os.path.getsize(movie_path),
                'info': '下午挺困的,可以提神醒脑'
            }
            data_json = json.dumps(data_dict)
            # 2.制作字典报头
            data_first = struct.pack('i', len(data_json))
            # 3.发送字典报头
            client.send(data_first)
            # 4.发送字典
            client.send(data_json.encode('utf8'))
            # 5.发送真实数据
            with open(movie_path,'rb') as f:
                for line in f:
                    client.send(line)
                    
                    
# 1.先接收固定长度为4的字典报头数据
        recv_first = sock.recv(4)
        # 2.解析字典报头
        dict_length = struct.unpack('i', recv_first)[0]
        # 3.接收字典数据
        real_data = sock.recv(dict_length)
        # 4.解析字典(json格式的bytes数据 loads方法会自动先解码 后反序列化)
        real_dict = json.loads(real_data)
        # 5.获取字典中的各项数据
        data_length = 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_length:
                data = sock.recv(1024)
                recv_size += len(data)
                f.write(data)

扩展知识

"""
在阅读源码的时候
	1.变量名后面跟冒号 表示的意思是该变量名需要指代的数据类型
    2.函数后更横杆加大于号表示的意思是该函数的返回值类型
"""

image

posted on 2022-01-12 18:39  耿蜀黍  阅读(234)  评论(0编辑  收藏  举报