Python网络编程基础及socket之TCP收发消息及文件
wptr33 2025-01-01 22:57 17 浏览
网络编程必须了解的基本概念
MAC地址:是全球唯一标示的网络接口,每一个网卡接口、交换机接口、路由器接口的mac地址均不相同。mac地址是通信子网内部相互通信的标识,交换机根据mac地址区分用户。mac地址是物理层的概念。
IP地址:ip是网络层的网络协议,通过路径检测和逻辑寻址等方法使得两个端系统(pc与服务器、手机与服务器、手机与PC等)可经由通信子网中多个节点传输数据。ip是跨越2个端系统跨越多个通信子网相互通信的前提。常见协议有IPv4和IPv6。
端口:端口是应用层的概念,ip解决了不同的端系统之间相互通信的问题,但进行网络通信的实际上是端系统内部的不同进程之间进行的,可能存在多个进程。为了区分端系统内部不同进程所以引入了端口的概念。有个比较形象的比方,ip地址好比大楼地址、端口好比房间号码。一条进程可使用多端口,但一个端口只能被一条进程独占使用,其他进程访问被已被占用的端口会报端口冲突。
TCP协议:是一种面向连接的、可靠的、基于字节流的传输层通信协议。建立连接需要三次握手,断开连接需要四次挥手。优点:有确认、窗口、重传、拥塞控制机制,传递数据可以保证稳定和可靠。缺点:效率低、占用系统资源高、易被攻击 。常见的TCP应用及其端口有ssh:22、ftp:21、smtp:25、http:80、telnet:23、https:443、MYSQL:3306等。
UDP协议:是用户数据报协议,提供面向事务的简单不可靠信息传送服务。使用UDP协议不用建立连接,它强调性能且不保证数据安全完整到达。优点传输速度快,比TCP协议安全性略高。缺点:不可靠,不稳定。常见的UDP应用及其端口有name server域名服务:53、bootps下载引导程序消息服务端:67、bootpc下载引导程序消息客户端:68、TFTP简单文件传输协议:69、RPC远程过程调用:111、NTP网络时间协议:123、SNMP简单网络管理协议:161、QQ:4000等。
TCP协议socket
- 服务端方法:
- 创建socket实例。
sk = socket.socket()
- 绑定ip和端口。
sk.bind(('127.0.0.1',9001))
说明:该方法有一个参数,类型是元组(元组的第一个元素是IP,第二个元素是端口)。无返回值。
- 开启监听。
sk.listen()
说明:该方法有一个参数,类型是int。可以指定指定系统允许暂未 accept 的连接数,超过后将拒绝新连接。未指定则自动设为合理的默认值,一般使用缺省值即可。无返回值。
- 接入客户端,被动接受TCP客户端连接,(阻塞式)等待连接的到来。
conn, ip_port = sk.accept()
说明:该方法无参数。返回2个值,第一个是socket实例,第二个是元组(元组的第一个元素是IP,第二个元素是端口)。如果server端需要服务多个客户,那么accept()应该放入循环,每次成功接入后开启新的线程为新的客户端服务。
- 客户端方法:
- 创建socket实例。
sk = socket.socket()
- 连接服务端。
sk.connect(('127.0.0.1',9001))
说明:该方法有一个参数,类型是元组(元组的第一个元素是IP,第二个元素是端口)。无返回值。
- 公共方法:
- 发送数据。
sk.sendall()
- 接收数据。
sk.recv()
- 案例代码:以下写一个回声的简单案例,服务端多线程提供服务,接收客户端发来的信息,并返回时间戳。
- 服务端
import socket
from threading import Thread
from threading import enumerate as en
import time
HOST = '127.0.0.1' # IP地址
PORT = 50007 # 端口
max_connect = 5 # 最大连接数
def talk(conn):
while True:
data = conn.recv(1024).decode()
if data == 'q' or data == 'Q':
return
conn.sendall(
f"服务端于{time.strftime('%Y年%m月%d日%H时%M分%S秒')}收到了你发来的“{data}”信息!".encode())
def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
while True:
conn, _ = s.accept()
if len(en()) < max_connect: # 如果线程数未超过最大连接数,那么开启线程接入客户端提供服务
Thread(target=talk, args=(conn,)).start()
if __name__ == "__main__":
main()
- 客户端
import socket
import sys
HOST = '127.0.0.1' # IP地址
PORT = 50007 # 端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
while True:
message = input("输入你想发给服务端的信息:")
if not message:
continue
s.sendall(message.encode())
if message == 'q' or message == 'Q':
break
data = s.recv(1024).decode()
print(f'收到服务端发来的信息:{data}')
- 常见问题:
- 粘包问题:因为TCP协议是流式协议所以数据包之间没有边界,那么有时会因为操作系统缓存机制、网络延迟等原因造成2次间隔时间较短、数据量较少的数据合并成一次发送。因而影响了数据的完整性。
- 解决策略:常见的解决方式是通过自定义协议厘清数据包之间的边界。常见的方法是发送数据前先发4字节的数据包长度然后再发信息,接收时先接收4字节的数据包长度再按长度接收信息。
- 发送方: 1.发送数据包前先计算长度,再将int型长度数据转换成4字节的bytes型; 2.先发送4字节bytes型长度数据,再发送数据包。
- 接收方: 1.先接收4字节bytes型长度数据,将其转换成int型长度数据。 2.只接收指定长度的数据。 以上协议是服务端和终端双方均要遵守的自定义协议。这样就可以解决粘包问题。
- stuct模块pack和unpack缺陷:处理粘包问题我查阅了很多资料,看到绝大多数人都是import struct,使用struct.pack和unpack来完成int数据与bytes相互转换的工作。但是我觉得struct模块的pack和unpack有2个缺陷:一是表示数值范围是-2147483648至2147483647,负值在计算数据长度完全用不上,会造成上传、下载文件大小不能超过2个G,unpack返回的是一个元组,还要对元组解包才能使用。所以我尝试自己写了一个pack和unpack在下面分享给大家。
- 自定义pack和unpack
def pack(n):
if n >= 4294967296 or n < 0:
raise ValueError('The value is out of range.')
values = ((0b11111111000000000000000000000000, 24),
(0b111111110000000000000000, 16), (0b1111111100000000, 8), (0b11111111, 0))
ret = b''
for i in values:
x = (n & i[0]) >> i[1]
x = x.to_bytes(length=1, byteorder="big")
ret += x
return ret
def unpack(numlist):
if len(numlist) != 4:
raise ValueError("The bytes length must be 4.")
values = (24, 16, 8, 0)
ret = 0
i = 0
while i < 4:
n = numlist[i]
ret += n << values[i]
i += 1
return ret
- 代码说明: 自定义的pack函数表示范围是0-4294967295,上传、下载文件长度在4个g以内都不会报错。 在该函数中通过按位与配合位移算法以及python3内置函数to_bytes()来完成功能,不需要另外import。 自定义的unpack函数直接返回int型数值,不需要解包。在这个函数里全部是自定义的代码,没有引用任何函数也没有导包,通过位移运算完成bytes转换成int。
- 自定义类处理收发消息和文件
- 编写TCP协议socket应用时经常会遇到发送消息和发送文件2种需求,如果将发送文件和发送消息封装到类中,会很方便。
- 代码案例:
- common.py文件,存放了Transeiver类:
from os.path import getsize # 导入os模块中getsize函数,方便检查文件大小
class Transceiver:
def __init__(self, conn, path='.', buffer=65536) -> None:
self.conn = conn # 绑定接口
self.path = path # 绑定工作目录
self.buffer = buffer # 绑定文件缓冲区大小
@staticmethod # 静态方法
def pack(n: int) -> bytes:
"""将int型长度值转换成4字节bytes型数据"""
if n >= 4294967296 or n < 0:
raise ValueError('The value is out of range.')
values = ((0b11111111000000000000000000000000, 24),
(0b111111110000000000000000, 16), (0b1111111100000000, 8), (0b11111111, 0))
ret = b''
for i in values:
x = (n & i[0]) >> i[1]
x = x.to_bytes(length=1, byteorder="big")
ret += x
return ret
@staticmethod # 静态方法
def unpack(numlist: bytes) -> int:
"""将4字节bytes型转换回int型长度值"""
if len(numlist) != 4:
raise ValueError("The bytes length must be 4.")
values = (24, 16, 8, 0)
ret = 0
i = 0
while i < 4:
n = numlist[i]
ret += n << values[i]
i += 1
return ret
def send(self, message: str) -> None:
"""发送字符串消息"""
if (type(message) is str): # 判断消息类型
message = message.encode() # 将消息转成bytes型,缺省参数是utf8编码
self.conn.sendall(self.pack(len(message))) # 发送消息前先发消息的长度
self.conn.sendall(message) # 发送消息
@property
def recv(self) -> str:
length = self.unpack(self.conn.recv(4))
return self.conn.recv(length).decode() # 返回消息字符串
def send_file(self, name: str) -> int:
"""发送文件"""
try:
length = getsize(self.path + '/' + name)
if length > 4294967295: # 文件大小超过4gb,返回 -2 ,停止发送文件
return -2
self.send(name + ',' + str(length))
with open(self.path + '/' + name, mode='rb') as f: # 二进制读模式打开指定文件
while length >= 0: # 如果未发送的文件数据长度大于等于0则循环
file = f.read(self.buffer) # 从文件中读取指定大小的文件数据
self.conn.sendall(file) # 发送文件数据
length -= self.buffer # 计算未发送的文件数据长度
except FileNotFoundError:
return -1 # 文件找不到,返回 -1 ,停止发送文件
return 1 # 文件正常发送完毕,返回1
def recv_file(self) -> int:
"""接收文件"""
try:
name, length = self.recv.split(',') # 获取文件名和文件长度
with open(self.path + '/' + name, mode='wb') as f: # 接收文件写入指定目录下
size = 0 # 计算接收的数据大小
length = int(length) # 文件长度
while length > size: # 文件长度大于已接收的数据长度则循环
file = self.conn.recv(self.buffer) # 接收文件数据
f.write(file) # 写入文件数据
size += len(file) # 累加已接收到的文件数据大小
except FileNotFoundError:
return -1 # 文件找不到,返回 -1 ,停止发送文件
return 1 # 文件正常接收完毕,返回1
- server.py文件,服务端代码
import socket
from threading import Thread
from threading import enumerate as en
import time
import common
HOST = '127.0.0.1' # IP地址
PORT = 50007 # 端口
max_connect = 5
def talk(conn):
while True:
m = common.Transceiver(conn, path='d:/2')
data = m.recv
if data == 'q' or data == 'Q':
return
elif data == 'file' or data == 'FILE':
m.recv_file()
else:
m.send(
f"服务端于{time.strftime('%Y年%m月%d日%H时%M分%S秒')}收到了你发来的“{data}”信息!")
def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind((HOST, PORT))
s.listen()
while True:
conn, _ = s.accept()
if len(en()) <= max_connect:
Thread(target=talk, args=(conn,)).start()
if __name__ == "__main__":
main()
- client.py文件,客户端代码
import socket
import common
HOST = '127.0.0.1' # IP地址
PORT = 50007 # 端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
m = common.Transceiver(s, path='d:/1')
while True:
message = input("输入你想发给服务端的信息:")
if not message:
continue
m.send(message)
if message == 'q' or message == 'Q':
break
elif message == 'file' or message == 'FILE':
name = input('请输入文件名:')
check = m.send_file(name)
if check == 1:
print('文件发送成功!')
elif check == -1:
print('该文件找不到')
elif check == -2:
print('该文件超过4GB,太大了,不能发送!')
continue
data = m.recv
print(f'收到服务端发来的信息:{data}')
- 认真阅读并理解上述代码,从中可以发现使用类的好处。在common.py文件中定义了Transceiver收发器类。接下来在服务端和客户端中发送消息和发送文件代码都很简洁,提高了代码复用率。假设需要增加需求,发送接收文件需要校验md5,那么只需要在Transceiver收发器类中修改send_file方法和recv_file方法即可,而客户端和服务端的代码无须改动,这样面向对象的编程方法可维护性极高。
相关推荐
- 【推荐】一款开源免费、美观实用的后台管理系统模版
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍...
- Android架构组件-App架构指南,你还不收藏嘛
-
本指南适用于那些已经拥有开发Android应用基础知识的开发人员,现在想了解能够开发出更加健壮、优质的应用程序架构。首先需要说明的是:AndroidArchitectureComponents翻...
- 高德地图经纬度坐标批量拾取(高德地图批量查询经纬度)
-
使用方法在桌面上新建一个index.txt文件,把下面的代码复制进去保存,再把文件名改成index.html保存,双击运行打开即可...
- flutter系列之:UI layout简介(flutter ui设计)
-
简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。...
- Android开发基础入门(一):UI与基础控件
-
Android基础入门前言:...
- iOS的布局体系-流式布局MyFlowLayout
-
iOS布局体系的概览在我的CSDN博客中的几篇文章分别介绍MyLayout布局体系中的视图从一个方向依次排列的线性布局(MyLinearLayout)、视图层叠且停靠于父布局视图某个位置的框架布局(M...
- TDesign企业级开源设计系统越发成熟稳定,支持 Vue3 / 小程序
-
TDesing发展越来越好了,出了好几套组件库,很成熟稳定了,新项目完全可以考虑使用。...
- WinForm实现窗体自适应缩放(winform窗口缩放)
-
众所周知,...
- winform项目——仿QQ即时通讯程序03:搭建登录界面
-
上两篇文章已经对CIM仿QQ即时通讯项目进行了需求分析和数据库设计。winform项目——仿QQ即时通讯程序01:原理及项目分析...
- App自动化测试|原生app元素定位方法
-
元素定位方法介绍及应用Appium方法定位原生app元素...
- 61.C# TableLayoutPanel控件(c# tabcontrol)
-
摘要TableLayoutPanel在网格中排列内容,提供类似于HTML元素的功能。TableLayoutPanel控件允许你将控件放在网格布局中,而无需精确指定每个控件的位置。其单元格...
- 12个python数据处理常用内置函数(python 的内置函数)
-
在python数据分析中,经常需要对字符串进行各种处理,例如拼接字符串、检索字符串等。下面我将对python中常用的内置字符串操作函数进行介绍。1.计算字符串的长度-len()函数str1='我爱py...
- 如何用Python程序将几十个PDF文件合并成一个PDF?其实只要这四步
-
假定你有一个很无聊的任务,需要将几十个PDF文件合并成一个PDF文件。每一个文件都有一个封面作为第一页,但你不希望合并后的文件中重复出现这些封面。即使有许多免费的程序可以合并PDF,很多也只是简单的将...
- Python入门知识点总结,Python三大数据类型、数据结构、控制流
-
Python基础的重要性不言而喻,是每一个入门Python学习者所必备的知识点,作为Python入门,这部分知识点显得很庞杂,内容分支很多,大部分同学在刚刚学习时一头雾水。...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
面试官:git pull是哪两个指令的组合?
-
git pull命令使用实例 git pull--rebase
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mysql max (33)
- vba instr (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)