初始网络编程

网络通信基础

CS架构与BS架构

在互联网中,通信协议一般分为两种:CS架构以及BS架构

CS指的是Client Server 代表客户端和服务端
BS指的是Browser Server 代表浏览器和服务端

客户端send                       服务端recv
操作系统                          操作系统
计算机硬件   <=====物理介质=====>  计算机硬件

网络通信

网络 = 物理链接介质 + 互联网通信协议

网络存在的意义就是跨地域数据传输, 称之为通信
互联网的本质就是一系列的网络协议
互联网协议的功能:定义计算机如何接入internet,以及接入internet的计算机通信的标准。

体系结构

回顾总结

网络编程
互联网协议   --- 七层 :osi协议

五层
应用层       python
传输层       tcp/udp
网络层       ip
数据链路层    arp
物理层
arp 通过ip找mac地址
交换机:广播 单播 组播
ip协议: ip地址的格式
 - ip地址 一台地址在一个网络内唯一的标识
 - 子网掩码 ip地址与子网掩码做按位与运算,得到的结果是网段
 - 网关ip 局域网内的机器访问公网ip,就通过网关访问
 
tcp 面向流的 可靠 全双工 三次握手 四次回收 ---> 粘包
udp 面向数据包 不可靠

黏包
什么是粘包
怎么解决
 - 在发送信息之前 先告诉对方要发的数据有多大
 - struct模块将要发送数据的大小固定化,无论如何就发四个字节
 - 自定义协议的概念
 
 验证客户端的合法性
 hmac
 
s.setblocking()
 
处理并发问题
 socketserver
 
import socket

sk = socket.socket()
sk.setblocking(False)  # 如果不给我链接,我就报错 设置成False的情况下
sk.bind(('127.0.0.1', 8080))
sk.listen()

conn, addr = sk.accept()  # BlockingIOError: [WinError 10035] 无法立即完成一个非阻止性套接字操作。

把计算机网络的各层及其协议的集合,称为网络的体系结构

互联网通信协议根据历史发展分别有下面几个协议

注:图片源于知乎 友情链接

image-20240112151439013image-20240112151459443

物理层

功能:负责传输比特流,将数据从物理介质上发送和接收。

两台计算机如果想要通信,就必须接入Internet(接入网络)

image-20240112152547800

数据链路层

功能:负责在直接相连的节点之间传输数据帧,进行错误检测和纠正,以及流量控制。

网络层

目的:划分广播域

功能:负责为数据包选择合适的路径,并进行路由和转发。

引入一套新的地址来区分不同的广播域/子网,这套地址即网络地址(IP地址)

网络层:遵循IP协议

IP协议

IP协议是一种用于在网络上寻址和路由数据包的协议,它定义了如何将数据包从源地址发送到目标地址。

主要作用
为每一台计算机分配IP地址
确定哪些地址在同一个网络

注意:单纯的IP地址段只是标识了IP地址的种类,从网络部分或主机部分都无法辨识一个IP所处的子网

例如:172.18.45.1与172.18.45.2 并不能确定二者处于同一子网

子网掩码

子网掩码是一个用于确定IP地址的网络部分和主机部分的分界线的32位二进制数字,它与IP地址进行逻辑与操作,用于确定一个IP地址所属的网络。

IP数据包

IP数据包是在网络上传输的数据单元,它包含了源地址和目标地址等信息,用于在互联网上路由和传递数据。

ARP协议

ARP协议(Address Resolution Protocol)用于将IP地址解析为对应的物理MAC地址,以便在局域网内部进行通信。

补充1:arp协议只在一个局域网内通过ip解析获取mac地址,不会跨局域网根据ip地址获取主机mac地址。
补充2:局域网内每个主机的ip地址各不相同,这才实现了arp协议根据ip地址解析成mac地址的可能。
补充3:ip地址本质是网络地址加主机地址。通过ip地址可以唯一确定一个机器,因为局域网内每个主机的mac地址不同就会分配不同的ip地址。

传输层

功能:建立端口到端口的通信

传输层遵循的协议有两个:TCP协议和UDP协议,基于端口工作

mac + ip 标识唯一一个主机
ip + port 标识唯一一台主机上的一个基于网络通信的软件

TCP头里面放的是源端口和目标端口,UDP头也一样。

# tcp协议
可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
# 至此:数据包装结果
以太网头 --> ip头 --> tcp头 --> 数据   

# udp协议
不可靠传输,”报头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
# 至此:数据包装结果
以太网头 --> ip头 --> udp头 --> 数据

注意:tcp协议中 发送的每一条数据都不能为空 不然会引发错误。

TCP协议和UDP协议都是工作在传输层

image-20240112181318747

三次握手

image-20240112181434496

<1> 客户端作为主动方,首先向服务端发一个建立链接的请求。
<2> 服务端接收到请求后,回复同意。
<3> 然后服务端也要向客户端发一个请求建立链接。
<4> 客户端收到请求后,回复同意
<2>和<3>是两个连续从服务端发出的消息,可以合并从一个回复并请求。整个过程就是三次握手建立TCP连接

四次挥手

image-20240112181843875

第一次挥手:当客户端想要关闭连接时,发送一个带有FIN(结束)标志位的报文,表明自己已经没有数据要发送了。
第二次挥手:服务器收到客户端的关闭请求后,发送一个带有ACK标志位的应答报文,确认收到客户端的关闭请求。
第三次挥手:服务器关闭自己的数据传输,发送一个带有FIN标志位的报文,表明自己已经没有数据要发送了。
第四次挥手:客户端收到服务器的关闭请求后,发送一个带有ACK标志位的应答报文,确认收到服务器的关闭请求。

UDP协议

udp协议特点:

UDP是无连接的,即发数据之前不需要建立连接,同理发送数据结束也没有连接可释放,这极大减少了开销和发送数据之前的时延。

UDP不保证数据的可靠交付,只负责将数据尽可能快的发出去。

即可能会产生丢包,一般使用场景就是网络查询,视频通话等。

image-20240112182026164


应用层

应用层功能:提供特定的应用程序功能,例如电子邮件、文件传输、远程登录等。

我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程,在本地进程通讯中我们可以使用PID来唯一标示一个进程,但PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它径了,我们知道IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。

能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,什么是socket呢?我们经常把socket翻译为套接字,socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。

img

socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

总结

img

每层都有自己的协议

img


网络通信实现

要实现网络通信,每台主机需要具备以下四个要素:
1. 本机的IP地址:用于唯一标识主机在网络中的位置。
2. 子网掩码:确定IP地址中哪些位用于网络标识,哪些位用于主机标识。
3. 网关的IP地址:作为主机与其他网络通信的出口,用于转发数据包到目标网络。
4. DNS的IP地址:用于域名解析,将域名转换为对应的IP地址。

这些要素可以通过两种方式获取:
1. 静态获取(手动配置):手动配置每个要素的数值,包括设置本机的IP地址、子网掩码、网关的IP地址和DNS的IP地址。
2. 动态获取(通过DHCP协议):使用DHCP(动态主机配置协议)协议,主机可以自动获取IP地址、子网掩码、网关的IP地址和DNS的IP地址。DHCP协议使用UDP协议进行通信。

DNS协议

DNS是域名解析服务器。也可以是域名解析系统,是用来绑定ip和域名的关系,它基于udp协议。

不管是BS架构还是CS架构的互联网通信软件都是基于IP地址+端口工作的。
对于BS架构,从方便使用者的角度出发(不需要用户记住复杂无趣的ip,引入的域名的概念)。于是BS软件就需要DNS域名解析过程。
基于CS架构的软件不需要DNS。

img


网络通信流程

1. 本机获取网络配置信息,包括IP地址、子网掩码、网关和DNS服务器的IP地址。
2. 打开浏览器,输入要访问的网址(如www.google.com)。
3. 浏览器使用DNS协议将网址解析为IP地址。
4. 使用HTTP协议发送请求给目标服务器,请求中包含了请求的内容和相关参数。
5. 请求数据被分割为TCP数据包,并添加TCP标头信息。
6. TCP数据包被嵌入IP数据包,添加IP标头信息。
7. IP数据包被嵌入以太网数据包,添加以太网标头信息。
8. 以太网数据包通过网络传输到目标服务器。
9. 目标服务器接收数据包,并按照相反的顺序解析数据。
10. 目标服务器使用HTTP协议生成响应,将响应数据分割为TCP数据包,并添加标头信息。
11. 响应数据通过网络返回到本机。
12. 本机接收到响应数据,浏览器解析并显示网页内容。

img


总结

基于网络通信的软件有两种架构:BS架构和CS架构。它们在以下几个方面有所不同:

  1. 客户端软件:BS架构没有独立的客户端软件,而是依赖于浏览器作为特殊的客户端来访问应用程序。
  2. DNS域名解析:在BS架构中,客户端多了一个DNS域名解析过程,即在浏览器的网址输入栏中输入网址后,需要进行域名解析获取对应的IP地址。
  3. 网络通信流程:BS架构中的网络通信流程可以总结如下:
    • 本机的DNS域名解析系统启动,获取域名对应的IP地址,并将其交给浏览器。
    • TCP/IP封包过程:浏览器将输入栏中的虚拟路径和其他信息打包成TCP/IP数据包。
    • 数据包通过网络传输到对方主机,对方主机的应用层软件获取虚拟路径对应的资源,并原路返回响应给客户端。
    • 本地主机接收到响应信息,解包获取请求的资源。
    • 完成一次网络通信交流流程。

数据包装的过程如下:

  1. DNS协议启动工作,获取域名对应的IP地址。
  2. 应用层将数据打包成一个HTTPS协议的协议头,并交给传输层。
  3. 传输层基于TCP协议,将应用层的数据再打包成一个TCP头,其中包含浏览器端口和对方主机的端口,并将数据交给网络层。
  4. 网络层将数据打包成一个IP头,其中包含本机IP和对方主机的IP,并将包裹交给数据链路层。
  5. 数据链路层根据网络地址判断是外网IP,并在整个包裹上再打包一个以太网头,包含本机的MAC地址和当前局域网网关的MAC地址。
  6. 物理层将数据转换为二进制01数据,并通过网络传输到对方局域网的网关。经过交换机、路由系统等设备,根据IP地址找到目标主机,并根据端口号找到对应的软件,最终根据URL路径找到资源。
  7. 对方主机返回响应数据,按照原路返回资源。我方接收到响应资源后,完成了一次网络通信。

socket

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

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

AF_UNIX

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

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

AF_INET

基于网络类型的套接字家族

AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,故大多只使用AF_INET。

socket模块

基于网络类型的套接字,应用程序交流按照链接的有无,又分为基于tcp的套接字和基于udp的套接字

python中的socket模块帮我们封装了socket抽象层,并提供我们一些简单接口使用。

image-20240116180553552

图片源于网络_

建立TCP连接

# 服务端
import socket


# 创建一个TCP socket     # AF_INET代表ipv4家族   SOCK_STREAM代表TCP协议
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# IP地址和端口号, 端口号别给0-1024之间  IP地址如果是0.0.0.0 表主机的任意网卡都可以使用socket通信
server_address = ("127.0.0.1", 8080)
# 绑定地址和端口
server_socket.bind(server_address)
# 将socket设置为监听状态,并等待客户端链接,默认值为 socket.SOMAXCONN。
# socket.SOMAXCONN 是一个系统定义的常量,表示操作系统允许的最大连接数。
server_socket.listen()
print("链接已创建 等待客户端链接...")
try:
    # 使用 accept() 方法接受客户端的连接,返回一个新的 Socket 对象和客户端地址。
    client_socket, client_address = server_socket.accept()
    print(f"客户端 [{client_address}] 已成功连接! ")
    # 和客户端通信 这里要用客户端去回信 不然会报错 记得消息是二进制的
    data = client_socket.recv(1024).decode()
    print(f"来自客户端的消息:{data}")
    client_socket.send("客户端 你好 ^_^".encode())
finally:
    # 依次关闭客户端和服务端
    client_socket.close()
    server_socket.close()
# 客户端 
import socket

 
# 创建socket  # AF_INET代表ipv4家族   SOCK_STREAM代表TCP协议
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 服务器的地址和端口
server_address = ("127.0.0.1", 8080)
try:
    # 链接服务器
    client_socket.connect(server_address)
    # 给服务器发送数据 注意要编码
    message = "服务器 你好!"
    client_socket.send(message.encode()) # 这里如果不指定编码格式 默认就是utf-8
    # 接收服务器响应
    response = client_socket.recv(1024) # 单位是字节
    print(f"来自服务器的消息:{response.decode()}")
except socket.error as e:
    print(f"发生了错误: {e}")
finally:
    client_socket.close()

image-20240112193930059

循环发送消息

# 服务端
from socket import *

server = socket()
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

server.bind(("127.0.0.1", 8080))
server.listen()

print("链接已创建 等待客户端链接...")
conn, addr = server.accept()

try:
    while data := conn.recv(1024):
        print(f"来自客户端{addr}的消息:{data.decode()}")
        message = input("输入需要发送给客户端的消息:")
        conn.send(message.encode())
    else:
        print("系统提示:客户端已下线,链接已关闭...")
except (OSError, ConnectionResetError, KeyboardInterrupt) as e:
    print(e)

finally:
    conn.close()
    server.close()

# 客户端
import socket


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

try:
    while True:
        message = input("输入需要发送给服务器的消息:")
        if len(message) < 1:
            print("您已主动已关闭连接")
            break
        client.send(message.encode())
        response = client.recv(1024).decode()
        print(f"来自服务器的消息:{response}")

except (ConnectionResetError, OSError, KeyboardInterrupt) as e:
    print(e)
finally:
    client.close()

image-20240112203313356

UDP方式发送

# server.py
import socket

# 创建UDP socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    # 绑定IP地址和端口号
    s.bind(("127.0.0.1", 8080))
    
    # 接收数据和客户端地址
    data, addr = s.recvfrom(1024)
    
    # 打印客户端地址和接收到的数据(解码)
    print("Client Address:", addr)
    print("Received Data:", data.decode())

# client.py
import socket

# 创建UDP socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as c:
    # 服务器地址
    server_address = ("127.0.0.1", 8080)
    
    # 发送数据到服务器
    c.sendto("你好,我是UDP".encode(), server_address)

粘包

为什么会出现黏包

首先只有再TCP协议中才会出现黏包现象

是因为TCP协议是面向流协议

再发送数据传输的过程中还有缓存机制来避免数据丢失

因此 在连续发送小数据的时候 以及接收大小不符的时候都容易出现黏包

本质还是因为我们在接收数据的时候还不知道发送数据的长短

解决黏包问题

在传输大量数据之前先告诉接收端要发送的数据大小

如果想要更漂亮的解决问题,可以通过struct模块来定制协议

struct模块

pack 和 unpack

模式 i

pack之后的长度 4个字节

unpack之后拿到的数据是一个元组:元组的第一个元素才是pack的值

UDP永远不会黏包  UDP会丢包
TCP会黏包

我们在网络上传输的所有数据 都叫数据包
数据包里面所有的数据 都叫报文
报文里面不止有你的数据  还有 ip地址 mac地址 端口号
所有的报文 都有 报头

所有的协议 都有报头 接收多少个字节 等。
我自己定义报文
	- 比如说 复杂的应用上就会用到
	- 比如说传入文件的时候就够复杂了
		- 文件的名字
		- 文件的大小
		- 文件的类型
		- 存储路径
		
head = {'filename': 'test.txt', 'filesize':405050, ''}
# 先发送报头的长度   # 先接收4个字节
# send(head) 报头  # 根据4个字节获取报头
# send(file) 报文  # 从报头中获取filesize 然后根据filesize 接收文件

其实在网络传输的过程当中  处处有协议 (操作系统做好了)
协议就是一堆报文和报头  --> 字节
协议的解析过程我们不需要关心
如果跳出我所了解的端口 IP 协议
我们写的程序也需要多次发送数据或者发送多个数据
我们也可以自定义协议 --- 本质上就是一种约定


如果写一个项目 使用socket 应该把IP地址和端口号写道一个配置文件里面 比如config

解决粘包

# 服务端 接收数据
import struct
from socket import *
import subprocess

server = socket()
ip_port = ('127.0.0.1', 713)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(ip_port)
server.listen()
print("服务器已创建 等待客户端链接...")
conn, addr = server.accept()
print(f"{addr}链接成功,正在进行数据交互")
try:
    while True:
        cmd = conn.recv(1024).decode()
        res = subprocess.run(cmd,
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        if cmd == 'q':
            print("客户端已关闭链接,服务端也正常关闭了链接。")
            break

        print(f"{addr}\t\t|\t\t{cmd}")

        std_err = res.stderr
        std_out = res.stdout

        length = len(std_out) + len(std_err)
        data_len = struct.pack('i', length)
        conn.send(data_len)
        conn.send(std_err)
        conn.send(std_out)

except (OSError, KeyboardInterrupt, ConnectionResetError) as e:
    print("发生了错误", e)
finally:
    conn.close()
    server.close()
      
# 客户端 发送数据

import socket
import struct

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

while True:
    try:
        cmd = input(">>>").strip()
        if cmd == 'q':
            client.send('q'.encode())
            break

        client.send(cmd.encode())
        data = client.recv(4)
        msg = struct.unpack('i', data)[0]
        rel_msg = client.recv(msg).decode('gbk')
        print(rel_msg)

    except (OSError, ConnectionResetError, KeyboardInterrupt) as e:
        print("发生了错误", e)
        break

小练习

网盘

文件的上传下载

server端 和client端
登录 客户端登录 将用户名和密码发给服务端 服务端确认消息知乎 
上传下载

选择上传 / 下载
上传:选择要上传的文件路径,在server创建一个同名的空文件.cache
下载:选择要下载的文件路径,在client端创建一个同名的空文件.cache
# 发送端
import socket
import os
import json
import struct

buffer = 1024

sk = socket.socket()
sk.connect(('192.168.1.195', 8080))



# 发送文件
head = {'filepath': r"E:\微信数据\mp4资源",
        'filename': r'1a5b6d5d21a08219c2b3beafd4a851a7.mp4',
        'filesize': None
        }


file_path = os.path.join(head['filepath'], head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize

json_head = json.dumps(head)  # 字典序列化
bytes_head = json_head.encode('utf-8') # 转bytes

# 计算head的长度
head_len = len(bytes_head)
pack_len = struct.pack('i', head_len)
sk.send(pack_len) # 先发报头的长度
sk.send(bytes_head) # 再发送bytes类型的报头

with open(file_path, 'rb') as f:
    while filesize:
        if filesize >= buffer:
            content = f.read(buffer)  # 每次读出来的内容
            sk.send(content)
            filesize -= buffer
        else:
            f.read(filesize)
            sk.send(content)
            break

sk.close()
# 接收端
import socket
import json
import struct

buffer = 1024

sk = socket.socket()
sk.bind(('192.168.1.195', 8080))
sk.listen()

conn, addr = sk.accept()
# 接收
head_len = conn.recv(4)
head_len = struct.unpack('i', head_len)[0]
json_head = conn.recv(head_len).decode()
head = json.loads(json_head)
filesize = head['filesize']


with open(head['filename'], 'wb') as f:
    while filesize:
        if filesize >= buffer:
            content = conn.recv(buffer)
            f.write(content)
            filesize -= buffer
        else:
            conn.recv(filesize)
            f.write(content)
            break
    
conn.close()
sk.close()

解决OSError

#加入一条socket配置,重用ip和端口
import socket
from socket import SOL_SOCKET,SO_REUSEADDR

sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
sk.bind(('127.0.0.1',8898))  #把地址绑定到套接字
sk.listen()          #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024)   #接收客户端信息
print(ret)              #打印客户端信息
conn.send(b'hi')        #向客户端发送信息
conn.close()       #关闭客户端套接字
sk.close()        #关闭服务器套接字(可选)

验证客户端的合法性hmac

import hmac  # 和hashlib一样的
h = hmac.new()  # secret_key  你想进行加密的bytes
密文 = h.digest()

hmac.compare_digest() # 对比密文 另外一个密文

代码实现

import socket
import os
import hmac
import hashlib

secret_key = b'egg'

# 创建一个套接字对象并绑定到指定的IP地址和端口号
server = socket.socket()
server.bind(('127.0.0.1', 8080))

# 开始监听连接请求
server.listen()
print('服务端已建立,等待客户端连接。。。')

# 接受客户端的连接请求
conn, addr = server.accept()


def check(conn):
    # 生成随机的消息
    msg = os.urandom(32)

    # 发送消息给客户端
    conn.send(msg)

    # 使用 HMAC 算法计算消息的摘要
    h = hmac.new(key=secret_key, msg=msg, digestmod=hashlib.md5)
    digest = h.digest()

    # 接收客户端发送的摘要
    client_digest = conn.recv(1024)

    # 比较服务器端计算的摘要与客户端发送的摘要是否一致
    return hmac.compare_digest(digest, client_digest)


# 调用check函数进行验证
res = check(conn)

# 根据验证结果输出相应的信息
if res:
    print("合法的客户端")
    server.close()
else:
    print("非法的客户端,已终止连接。")
    server.close()

# 关闭连接
conn.close()

# 关闭服务器套接字
server.close()
import socket
import hmac
import hashlib

secret_key = b'egg'

# 创建一个套接字对象并连接到服务器
client = socket.socket()
client.connect(('127.0.0.1', 8080))

# 接收服务器发送的消息
msg = client.recv(1024)

# 使用 HMAC 算法计算消息的摘要
h = hmac.new(key=secret_key, msg=msg, digestmod=hashlib.md5)
digest = h.digest()

# 发送摘要给服务器
client.send(digest)

# 关闭套接字连接
client.close()

结果1 secret值一样 验证成功 可以续写后面的逻辑

image-20240116144712415

结果2 我把服务端的secret改成其它的 比如eggggggggg 验证失败

image-20240116144904654

小拓展

os.urandom(int) 随机生成指定位数的字节

import os

os.urandom(32)
Out[2]: b'\x04*\xd1\x96`\x02sb\xb9-LN\xe5\x8a^r\xa9\xea\xe6\xcbk\xd68\x042\xa1\xcd\xca$\xfc\xf4\xcc'

socketserver

socketserver就是长久的进行链接,和多个客户端及时通信

# config.py
IP = '127.0.0.1'
PORT = 9821
# 服务端
import socketserver
from socket import SOL_SOCKET, SO_REUSEADDR
from config import IP, PORT


class MyServer(socketserver.BaseRequestHandler):  # 必须继承它
    def handle(self) -> None:  # 必须有一个叫handle的方法
        # 此处的self.request 就相当于一个conn
        # 所有的操作都应该用self.request去做
        while True:
            addr = self.client_address
            msg = self.request.recv(1024).decode()
            print(f'来自客户端{addr}的消息:{msg}')
            # info = input(">>>").strip()
            # self.request.send(info.encode())


if __name__ == '__main__':
    print("服务器已启动, 等待客户端链接...")
    try:
        server = socketserver.ThreadingTCPServer((IP, PORT), MyServer)  # 要传入端口以及类名
        server.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
        # Thread 线程的意思
        # 引用了线程的概念,实现并发的效果
        # 正常情况下,一个程序只有一个线程
        # 也就是正常情况下,socket服务端无法和多个客户端进行通信
        # processes 进程
        server.serve_forever()  # 永远起一个服务 然后一直运行
    except ConnectionResetError:
        print("全部客户已关闭链接,服务器已结束运行。")
        MyServer.request.shutdown()

# qq udp协议
# bind listen
# conn, addr = accept
# self.request = conn

实测退出是无效的,也就是印证了这个模块是需要服务端一直工作的。

# 服务端
import socket
from config import IP, PORT


client = socket.socket()
client.connect((IP, PORT))

try:
    while True:
        msg = input(">>>")
        if msg.casefold() == 'q':
            client.send(b'q')
            break

        client.send(msg.encode())
        ret = client.recv(1024).decode()
        print(ret)

except (ConnectionResetError, KeyboardInterrupt, OSError) as e:
    print(e)
finally:
    client.close()

image-20240116200406043

posted @ 2024-01-14 20:43  小满三岁啦  阅读(18)  评论(0编辑  收藏  举报