百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Python网络编程基础及socket之TCP收发消息及文件

wptr33 2025-01-01 22:57 35 浏览

网络编程必须了解的基本概念

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

  • 服务端方法
  1. 创建socket实例。
sk = socket.socket() 
  1. 绑定ip和端口。
sk.bind(('127.0.0.1',9001))

说明:该方法有一个参数,类型是元组(元组的第一个元素是IP,第二个元素是端口)。无返回值。

  1. 开启监听。
sk.listen()

说明:该方法有一个参数,类型是int。可以指定指定系统允许暂未 accept 的连接数,超过后将拒绝新连接。未指定则自动设为合理的默认值,一般使用缺省值即可。无返回值。

  1. 接入客户端,被动接受TCP客户端连接,(阻塞式)等待连接的到来。
conn, ip_port = sk.accept()

说明:该方法无参数。返回2个值,第一个是socket实例,第二个是元组(元组的第一个元素是IP,第二个元素是端口)。如果server端需要服务多个客户,那么accept()应该放入循环,每次成功接入后开启新的线程为新的客户端服务。

  • 客户端方法
  1. 创建socket实例。
sk = socket.socket() 
  1. 连接服务端。
sk.connect(('127.0.0.1',9001))

说明:该方法有一个参数,类型是元组(元组的第一个元素是IP,第二个元素是端口)。无返回值。

  • 公共方法
  1. 发送数据。
sk.sendall()
  1. 接收数据。
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方法即可,而客户端和服务端的代码无须改动,这样面向对象的编程方法可维护性极高。

相关推荐

[常用工具] git基础学习笔记_git工具有哪些

添加推送信息,-m=messagegitcommit-m“添加注释”查看状态...

centos7安装部署gitlab_centos7安装git服务器

一、Gitlab介1.1gitlab信息GitLab是利用RubyonRails一个开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。...

太高效了!玩了这么久的Linux,居然不知道这7个终端快捷键

作为Linux用户,大家肯定在Linux终端下敲过无数的命令。有的命令很短,比如:ls、cd、pwd之类,这种命令大家毫无压力。但是,有些命令就比较长了,比如:...

提高开发速度还能保证质量的10个小窍门

养成坏习惯真是分分钟的事儿,而养成好习惯却很难。我发现,把那些对我有用的习惯写下来,能让我坚持住已经花心思养成的好习惯。...

版本管理最好用的工具,你懂多少?

版本控制(Revisioncontrol)是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。...

Git回退到某个版本_git回退到某个版本详细步骤

在开发过程,有时会遇到合并代码或者合并主分支代码导致自己分支代码冲突等问题,这时我们需要回退到某个commit_id版本1,查看所有历史版本,获取git的某个历史版本id...

Kubernetes + Jenkins + Harbor 全景实战手册

Kubernetes+Jenkins+Harbor全景实战手册在现代企业级DevOps体系中,Kubernetes(K8s)、Jenkins和Harbor组成的CI/CD流水...

git常用命令整理_git常见命令

一、Git仓库完整迁移完整迁移,就是指,不仅将所有代码移植到新的仓库,而且要保留所有的commit记录1.随便找个文件夹,从原地址克隆一份裸版本库...

第三章:Git分支管理(多人协作基础)

3.1分支基本概念分支是Git最强大的功能之一,它允许你在主线之外创建独立的开发线路,互不干扰。理解分支的工作原理是掌握Git的关键。核心概念:HEAD:指向当前分支的指针...

云效Codeup怎么创建分支并进行分支管理

云效Codeup怎么创建分支并进行分支管理,分支是为了将修改记录分叉备份保存,不受其他分支的影响,所以在同一个代码库里可以同时进行多个修改。创建仓库时,会自动创建Master分支作为默认分支,后续...

git 如何删除本地和远程分支?_git怎么删除远程仓库

Git分支对于开发人员来说是一项强大的功能,但要维护干净的存储库,就需要知道如何删除过时的分支。本指南涵盖了您需要了解的有关本地和远程删除Git分支的所有信息。了解Git分支...

git 实现一份代码push到两个git地址上

一直以来想把自己的博客代码托管到github和coding上想一次更改一次push两个地址一起更新今天有空查资料实践了下本博客的github地址coding的git地址如果是Gi...

git操作:cherry-pick和rebase_git cherry-pick bad object

在编码中经常涉及到分支之间的代码同步问题,那就需要cherry-pick和rebase命令问题:如何将某个分支的多个commit合并到另一个分支,并在另一个分支只保留一个commit记录解答:假设有两...

模型文件硬塞进 Git,GitHub 直接打回原形:使用Git-LFS管理大文件

前言最近接手了一个计算机视觉项目代码是屎山就不说了,反正我也不看代码主要就是构建一下docker镜像,测试一下部署的兼容性这本来不难但是,国内服务器的网络环境实在是恶劣,需要配置各种镜像(dock...

防弹少年团田柾国《Euphoria》2周年 获世界实时趋势榜1位 恭喜呀

当天韩国时间凌晨3时左右,该曲在Twitter上以“2YearsWithEuphoria”的HashTag登上了世界趋势1位。在韩国推特实时趋势中,从上午开始到现在“Euphoria2岁”的Has...