14.网络编程

互联网时代,现在基本上所有的程序都是网络程序,很少有单机版的程序了。

网络编程就是如何在程序中实现两台计算机的通信。

Python 语言中,提供了大量的内置模块和第三方模块用于支持各种网络访问,而且 Python 语言在网络通信方面的优点特别突出,远远领先其他语言。

> 通过本章,可以学到:
> 1. 了解 TCP 和 UDP > 2. 掌握编写 UDP Socket 客户端应用 > 3. 掌握编写 UDP Socket 服务器端应用 > 4. 掌握编写 TCP Socket 客户端应用 > 5. 掌握编写 TCP Socket 服务器端应用

1 IP 地址的介绍

1.1 IP 地址的概念

IP 地址就是标识网络中设备的一个地址,好比现实生活中的家庭地址。

IP 是 Internet Protocol Address,即 " 互联网协议地址 "。

网络中的设备效果图:

1.2 IP 地址的表现形式

目前主流使用的 IP 地址是 IPV4,但是随着网络规模的不断扩大,IPV4 面临着枯竭的危险,所以推出了 IPV6。

说明:

> IPV4,采用 32 位地址长度,只有大约 43 亿个地址,它只有 4 段数字,每一段最大不超过 255。随着互联网的发展,IP 地址不够用了,在 2019 年 11 月 25 日 IPv4 位地址分配完毕。
> IPv6 采用 128 位地址长度,几乎可以不受限制地提供地址。按保守方法估算 IPv6 实际可分配的地址,整个地球的每平方米面积上仍可分配 1000 多个地址。

IP 地址实际上是一个 32 位整数(称为 IPv4),以字符串表示的 IP 地址如 192.168.0.1 实际上是把 32 位整数按 8 位分组后的数字表示,目的是便于阅读。

IPv6 地址实际上是一个 128 位整数,它是目前使用的 IPv4 的升级版,以字符串表示类似于 2001:0db8:85a3:0042:1000:8a2e:0370:7334

  • IP 地址分为两类:IPv4 和 IPv6
  • IPv4 是目前使用的 ip 地址
  • IPv6 是未来使用的 ip 地址
  • IPv4 是由点分十进制组成
  • IPv6 是由冒号十六进制组成

1.3 IP 地址的作用

IP 地址的作用是标识网络中唯一的一台设备的,也就是说通过 IP 地址能够找到网络中某台设备。

用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。 比如互联网的每个服务器都要有自己的 IP 地址,而每个局域网的计算机要通信也要配置 IP 地址。

路由器是连接两个或多个网络的网络设备。

IP 地址作用效果图:

> IP 地址就像是我们的家庭住址一样,如果你要写信给一个人,你就要知道他(她)的地址,这样邮递员才能把信送到。计算机发送信息就好比是邮递员,它必须知道唯一的“家庭地址”才能不至于把信送错人家。只不过我们的地址是用文字来表示的,计算机的地址用二进制数字表示。
> IP 地址被用来给 Internet 上的电脑一个编号。大家日常见到的情况是每台联网的 PC 上都需要有 IP 地址,才能正常通信。我们可以把“个人电脑”比作“一台电话”,那么“IP 地址”就相当于“电话号码”,而 Internet 中的路由器,就相当于电信局的“程控式交换机”。

1.4 公有地址

公有地址(Public address)由 Inter NIC(Internet Network Information Center 互联网信息中心)负责。这些 IP 地址分配给注册并向 Inter NIC 提出申请的组织机构。通过它直接访问互联网。

1.5 私有地址

私有地址(Private address)属于非注册地址,专门为组织机构内部使用。

以下列出留用的内部私有地址

  • A 类 10.0.0.0--10.255.255.255
  • B 类 172.16.0.0--172.31.255.255
  • C 类 192.168.0.0--192.168.255.255
注意
- `127.0.0.1` 本机地址
> - `192.168.0.0--192.168.255.255` 为私有地址,属于非注册地址,专门为组织机构内部使用

1.6 查看 IP 地址

  • Linux 和 mac OS 使用 ifconfig 这个命令
  • Windows 使用 ipconfig 这个命令

说明:

ifconfig 和 ipconfig 都是查看网卡信息的,网卡信息中包括这个设备对应的 IP 地址

说明:

  • 192.168.1.107 是设备在网络中的 IP 地址
  • 127.0.0.1 表示本机地址,提示:如果和自己的电脑通信就可以使用该地址。
  • 127.0.0.1 该地址对应的域名是 localhost域名是 ip 地址的别名,通过域名能解析出一个对应的 ip 地址。

1.7 检查网络是否正常

  • 检查网络是否正常使用 ping 命令

检查网络是否正常效果图

说明:

  • ping www.baidu.com 检查是否能上公网
  • ping 当前局域网的ip地址 检查是否在同一个局域网内
  • ping 127.0.0.1 检查本地网卡是否正常

1.8 小结

  • IP 地址的作用是标识网络中唯一的一台设备的
  • IP 地址的表现形式分为:IPv4 和 IPv6
  • 查看网卡信息:ifconfig
  • 检查网络:ping

2 端口和端口号的介绍

2.1 问题思考

不同电脑上的飞秋之间进行数据通信,它是如何保证把数据给飞秋而不是给其它软件呢?

其实,每运行一个网络程序都会有一个端口,想要给对应的程序发送数据,找到对应的端口即可。

端口效果图:

2.2 什么是端口

端口是传输数据的通道,好比教室的门,是数据传输必经之路

那么如何准确的找到对应的端口呢?

其实,每一个端口都会有一个对应的端口号,好比每个教室的门都有一个门牌号,想要找到端口通过端口号即可。

端口号效果图:

2.3 什么端口号

操作系统为了统一管理这么多端口,就对端口进行了编号,这就是端口号,端口号其实就是一个数字,好比我们现实生活中的门牌号。

端口号用来识别计算机中进行通信的应用程序。因此,它也被称为程序地址。

端口号有 65536 个。

那么最终飞秋之间进行数据通信的流程是这样的,通过 ip 地址找到对应的设备,通过端口号找到对应的端口,然后通过端口把数据传输给应用程序

最终通信流程效果图:

一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地进行数据传输。

2.4 端口和端口号的关系

端口号可以标识唯一的一个端口。

2.5 端口分配

端口是虚拟的概念,并不是说在主机上真的有若干个端口。通过端口,可以在一个主机上运行多个网络应用程序。 端口的表示是一个 16 位的二进制整数,对应十进制的 0-65535

操作系统中一共提供了 0~65535 可用端口范围。

2.6 端口号的分类

  • 公认端口号(Well Known Ports)
  • 注册端口号(Registered Ports)

公认端口号:

知名端口号是指众所周知的端口号,范围从 0 到 1023。

  • 这些端口号一般固定分配给一些服务,比如 21 端口分配给 FTP(文件传输协议)服务,25 端口分配给 SMTP(简单邮件传输协议)服务,80 端口分配给 HTTP 服务。

注册端口号:

一般程序员开发应用程序使用端口号称为注册端口号, 范围是从 1024~65535

  • 如果程序员开发的程序没有设置端口号,操作系统会在动态端口号这个范围内随机生成一个给开发的应用程序使用。
  • 当运行一个程序默认会有一个端口号,当这个程序退出时,所占用的这个端口号就会被释放。

2.7 小结

  • 端口的作用就是给运行的应用程序提供传输数据的通道
  • 端口号的作用是用来区分和管理不同端口的,通过端口号能找到唯一个的一个端口
  • 端口号可以分为两类:公认端口号 和 注册端口号
    • 公认端口号的范围是 0~1023
    • 注册端口号的范围是 1024~65535

3 网络通信协议

国际标准化组织(ISO,即 International Organization for Standardization)定义了网络通信协议的基本框架,被称为 OSI(Open System Interconnect,即开放系统互联)模型。要制定通讯规则,内容会很多,比如要考虑 A 电脑如何找到 B 电脑,A 电脑在发送信息给 B 电脑时是否需要 B 电脑进行反馈,A 电脑传送给 B 电脑的数据格式又是怎样的?内容太多太杂,所以 OSI 模型将这些通讯标准进行层次划分,每一层次解决一个类别的问题,这样就使得标准的制定没那么复杂。OSI 模型制定的七层标准模型,分别是:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层

OSI 七层协议模型如图所示:

4 网络协议的分层

虽然国际标准化组织制定了这样一个网络通信协议的模型,但是实际上互联网通讯使用最多的网络通信协议是 TCP/IP 网络通信协议。

TCP/IP 是一个协议族,也是按照层次划分,共四层:应用层,传输层,互连网络层,网络接口层(物理 + 数据链路层)

把用户应用程序作为最高层,把物理通信线路作为最低层,将其间的协议处理分为若干层,规定每层处理的任务,也规定每层的接口标准。

ISO 模型与 TCP/IP 模型的对应关系如图所示。

5 TCP 和 UDP 协议区别

TCP 和 UDP 的优缺点无法简单地、绝对地去做比较:TCP 用于在传输层有必要实现可靠传输的情况UDP 主要用于那些对高速传输和实时性有较高要求的通信或广播通信。TCP 和 UDP 应该根据应用的目的按需使用。

5.1 TCP

TCP(Transmission Control Protocol,传输控制协议)。TCP 方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据

5.2 UDP

UDP(User Data Protocol,用户数据报协议)

UDP 是一个非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。在发送端,UDP 传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽的限制;在接收端,UDP 把每个消息段放在队列中,应用程序每次从队列中读一个消息段。

UDP 方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接传输也不是很可靠,如果发送失败则客户端无法获得

UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

5.3 TCP 和 UDP 区别

这两种传输方式都在实际的网络编程中使用,重要的数据一般使用 TCP 方式进行数据传输,而大量的非核心数据则可以通过 UDP 方式进行传递,在一些程序中甚至结合使用这两种方式进行数据传递。

由于 TCP 需要建立专用的虚拟连接以及确认传输是否正确,所以使用 TCP 方式的速度稍微慢一些,而且传输时产生的数据量要比 UDP 稍微大一些。

UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制 可靠传输,使用流量控制和拥塞控制
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅 8 字节 首部最小 20 字节,最大 60 字节
适用场景 适用于实时应用(IP 电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输
总结
- TCP 是面向连接的,传输数据安全,稳定,效率相对较低。
> - UDP 是面向无连接的,传输数据不安全,效率较高。

6 TCP 建立连接的三次握手

TCP 是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。一个 TCP 连接必须要经过三次“对话”才能建立起来,其中的过程非常复杂,只简单的描述下这三次对话的简单过程:

  1. 主机 A 向主机 B 发出连接请求:“我想给你发数据,可以吗?”,这是第一次对话;
  2. 主机 B 向主机 A 发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包:“可以,你什么时候发?”,这是第二次对话;
  3. 主机 A 再发出一个数据包确认主机 B 的要求同步:“我现在就发,你接着吧!”,这是第三次握手。

三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机 A 才向主机 B 正式发送数据。

  1. 第一步,客户端发送一个包含 SYN 即同步(Synchronize)标志的 TCP 报文,SYN 同步报文会指明客户端使用的端口以及 TCP 连接的初始序号。
  2. 第二步,服务器在收到客户端的 SYN 报文后,将返回一个 SYN+ACK 的报文,表示客户端的请求被接受,同时 TCP 序号被加一,ACK 即确认(Acknowledgement)
  3. 第三步,客户端也返回一个确认报文 ACK 给服务器端,同样 TCP 序列号被加一,到此一个 TCP 连接完成。然后才开始通信的第二步:数据处理。

这就是所说的 TCP 的三次握手(Three-way Handshake)。

> 为什么 TCP 协议有三次握手,而 UDP 协议没有?
> 因为三次握手的目的是在 client 端和 server 端建立可靠的连接。保证双方发送的数据对方都能接受到,这也是 TCP 协议的被称为可靠的数据传输协议的原因。而 UDP 就不一样,UDP 不提供可靠的传输模式,发送端并不需要得到接收端的状态,因此 UDP 协议就用不着使用三次握手。

7 TCP 断开连接的四次挥手

TCP 建立连接要进行 3 次握手,而断开连接要进行 4 次

  1. 当主机 A 完成数据传输后,将控制位 FIN 置 1,提出停止 TCP 连接的请求;
  2. 主机 B 收到 FIN 后对其作出响应,确认这一方向上的 TCP 连接将关闭,将 ACK 置 1;
  3. 由 B 端再提出反方向的关闭请求,将 FIN 置 1;
  4. 主机 A 对主机 B 的请求进行确认,将 ACK 置 1,双方向的关闭结束。

由 TCP 的三次握手和四次断开可以看出,TCP 使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

8 数据包与处理流程

8.1 什么是数据包

通信传输中的数据单位,一般也称“数据包”。在数据包中包括:包、帧、数据包、段、消息。

网络中传输的数据包由两部分组成:一部分是协议所要用到的首部,另一部分是上一层传过来的数据。首部的结构由协议的具体规范详细定义。在数据包的首部,明确标明了协议应该如何读取数据。反过来说,看到首部,也就能够了解该协议必要的信息以及所要处理的数据。包首部就像协议的脸。

8.2 数据包处理流程

9 socket 的介绍

9.1 问题思考

到目前为止我们学习了 ip 地址和端口号还有 tcp 传输协议,为了保证数据的完整性和可靠性我们使用 tcp 传输协议进行数据的传输,为了能够找到对应设备我们需要使用 ip 地址,为了区别某个端口的应用程序接收数据我们需要使用端口号,那么通信数据是如何完成传输的呢?

使用 socket 来完成

9.2 socket 的概念

socket (简称 套接字) 是进程之间通信一个工具,好比现实生活中的插座,所有的家用电器要想工作都是基于插座进行,进程之间想要进行网络通信需要基于这个 socket

插座效果图:

socket 效果图:

9.3 socket 的作用

负责进程之间的网络数据传输,好比数据的搬运工。

9.4 socket 使用场景

不夸张的说,只要跟网络相关的应用程序或者软件都使用到了 socket 。

9.5 socket 编程介绍

TCP 协议和 UDP 协议是传输层的两种协议。Socket 是传输层供给应用层的编程接口,所以 Socket 编程就分为 TCP 编程和 UDP 编程两类。

Socket 编程封装了常见的 TCP、UDP 操作,可以实现非常方便的网络编程。

9.6 socket() 函数介绍

在 Python 语言标准库中,通过使用 socket 模块提供的 socket 对象,可以在计算机网络中建立可以互相通信的服务器与客户端。在服务器端需要建立一个 socket 对象,并等待客户端的连接。客户端使用 socket 对象与服务器端进行连接,一旦连接成功,客户端和服务器端就可以进行通信了。

上图中,我们可以看出 socket 通讯中,发送和接收数据,都是通过操作系统控制网卡来进行。因此,我们在使用之后,必须关闭 socket。

在 Python 中,通常用一个 Socket 表示“打开了一个网络连接”,语法格式如下:

socket.socket([family[, type[, proto]]])

family:套接字家族可以使 AF_UNIX 或者 AF_INET

> AF 表示 ADDRESS FAMILY 地址族
> AF_INET(又称 PF_INET)是 IPv4 网络协议的套接字类型;而 AF_UNIX 则是 Unix 系统本地通信。

type:套接字类型可以根据是面向连接的还是非连接分为 SOCK_STREAMSOCK_DGRAM

protocol:一般不填,默认为 0。

Socket 主要分为面向连接的 Socket 和无连接的 Socket。

无连接 Socket 的主要协议是用户数据报协议,也就是常说的 UDP,UDP Socket 的名字是 SOCK_DGRAM。创建套接字 UDP/IP 套接字,可以调用 socket.socket()。示例代码如下:

udp_socket = socket.socket(AF_INET, SOCK_DGRAM)

9.7 socket 对象的内置函数和属性

在 Python 语言中 socket 对象中,提供如表所示的内置函数。

函数 描述
服务器端套接字
s.bind() 绑定地址(host,port)到套接字, 在 AF_INET 下,以元组(host,port)的形式表示地址。
s.listen() 开始 TCP 监听。backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为 1,大部分应用程序设为 5 就可以了。
s.accept() 被动接受 TCP 客户端连接,(阻塞式) 等待连接的到来
客户端套接字
s.connect() 主动初始化 TCP 服务器连接,。一般 address 的格式为元组(hostname,port),如果连接出错,返回 socket.error 错误。
s.connect_ex() connect() 函数的扩展版本,出错时返回出错码,而不是抛出异常
公共用途的套接字函数
s.recv() 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。
s.send() 发送 TCP 数据,将 string 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 string 的字节大小。
s.sendall() 完整发送 TCP 数据。将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。
s.recvfrom() 接收 UDP 数据,与 recv() 类似,但返回值是(data,address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。
s.sendto() 发送 UDP 数据,将数据发送到套接字,address 是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.close() 关闭套接字
s.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
s.getsockname() 返回套接字自己的地址。通常是一个元组 (ipaddr,port)
s.setsockopt(level,optname,value) 设置给定套接字选项的值。
s.getsockopt(level,optname[.buflen]) 返回套接字选项的值。
s.settimeout(timeout) 设置套接字操作的超时期,timeout 是一个浮点数,单位是秒。值为 None 表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 connect())
s.gettimeout() 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回 None。
s.fileno() 返回套接字的文件描述符。
s.setblocking(flag) 如果 flag 为 0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。
s.makefile() 创建一个与该套接字相关连的文件

9.8 小结

进程之间网络数据的传输可以通过 socket 来完成, socket 就是进程间网络数据通信的工具。

10 UDP 编程

10.1 UDP 编程介绍

UDP 协议时,不需要建立连接,只需要知道对方的 IP 地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。虽然用 UDP 传输数据不可靠,但它的优点是和 TCP 比,速度快,对于不要求可靠到达的数据,就可以使用 UDP 协议。

10.2 UDP 网络应用程序开发流程

创建 Socket 时,SOCK_DGRAM 指定了这个 Socket 的类型是 UDP。绑定端口和 TCP 一样,但是不需要调用 listen() 方法,而是直接接收来自任何客户端的数据。recvfrom() 方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用 sendto() 就可以把数据用 UDP 发给客户端。

UDP 网络应用程序开发流程的介绍

UDP 网络应用程序开发分为:

  • UDP 服务端程序开发
  • UDP 客户端程序开发

说明:

客户端程序是指运行在用户设备上的程序

服务端程序是指运行在服务器设备上的程序,专门为客户端提供数据服务。

UDP 服务端程序开发流程的介绍

步骤说明:

  1. 创建服务端端套接字对象
  2. 绑定端口号
  3. 等待接收数据
  4. 发送数据
  5. 关闭套接字

UDP 客户端程序开发流程的介绍

步骤说明:

  1. 创建客户端套接字对象
  2. 发送数据
  3. 接收数据
  4. 关闭客户端套接字

小结

  1. UDP 网络应用程序开发分为客户端程序开发服务端程序开发
  2. 主动发起建立连接请求的是客户端程序
  3. 等待接受连接请求的是服务端程序

10.3 UDP 服务端程序开发

开发 UDP 服务端程序开发步骤回顾

  1. 创建服务端端套接字对象
  2. 绑定端口号
  3. 等待接收数据
  4. 发送数据
  5. 关闭套接字

socket 类的介绍

导入 socket 模块

import socket

创建服务端 socket 对象

socket.socket(family, type)

参数说明:

  • family 表示 IP 地址类型,分为 IPv4 和 IPv6
  • Type 表示传输协议类型

方法说明:

  • bind((host, port)) 表示绑定端口号,host 是 ip 地址,port 是端口号;ip 地址一般不指定,表示本机的任何一个 ip 地址都可以。
  • sendto(data, address) 表示发送数据,data 是二进制数据;address 是一个二元组 (host, port) 表示接受者的地址和端口号。
  • recvfrom(bufsize) 表示接收数据,bufsize 是每次接收数据的长度。

UDP 服务端程序开发示例代码

"""  
最简化的 udp 服务器代码  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 等待接收数据  
4. 发送数据  
5. 关闭套接字  
"""  
# 导入 socket 包  
from socket import socket, AF_INET, SOCK_DGRAM  
  
# 1. 创建 socket 对象  
s = socket(AF_INET, SOCK_DGRAM)  
# 2. 绑定端口  
s.bind(('127.0.0.1', 8888))  
print('等待连接……')  
# 3. 等待接收数据  
recv_data = s.recvfrom(1024)  # 1024 是本次接受的最大字节数  
print(type(recv_data))  
print(recv_data)  
print(f'接收到远程信息{recv_data[0].decode()} from {recv_data[1]}')  
# 4. 发送数据  
s.sendto(b'hello,I am server', recv_data[1])  
# 5. 关闭套接字  
s.close()

执行结果:

说明:

当客户端和服务端建立连接后,服务端程序退出后端口号不会立即释放,需要等待大概 1-2 分钟。

解决办法有两种:

  1. 更换服务端端口号
  2. 设置端口号复用 (推荐大家使用),也就是说让服务端程序退出后端口号立即释放。

设置端口号复用的代码如下:

from socket import socket, SO_REUSEADDR, SOL_SOCKET
# 参数1: 表示当前套接字
# 参数2: 设置端口号复用选项
# 参数3: 设置端口号复用选项对应的值
udp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

小结

  1. 导入 socket 模块
  2. 创建 UDP 套接字 socket
    • 参数 1:AF_INET,表示 IPv4 地址类型
    • 参数 2:SOCK_DGRAM,表示 UDP 传输协议类型
  3. 绑定端口号 bind
    • 参数:元组,比如:(ip 地址, 端口号)
  4. 发送数据 sendto
    • 参数:要发送的二进制数据,注意:字符串需要使用 encode() 方法进行编码
  5. 接收数据 recvfrom
    • 参数:表示每次接收数据的大小,单位是字节,注意:解码成字符串使用 decode() 方法
  6. 关闭套接字 socket 表示通信完成

10.4 UDP 客户端程序开发

开发 UDP 客户端程序开发步骤回顾

  1. 创建客户端套接字对象
  2. 发送数据
  3. 接收数据
  4. 关闭客户端套接字

socket 类的介绍

导入 socket 模块

import socket

创建客户端 socket 对象

socket.socket(family, type)

参数说明:

  • family 表示 IP 地址类型,分为 TPv4 和 IPv6
  • type 表示传输协议类型

方法说明:

  • sendto(data) 表示发送数据,data 是二进制数据
  • recv(bufsize) 表示接收数据,bufsize 是每次接收数据的长度

UDP 客户端程序开发示例代码

"""  
最简 udp 客户端代码  
1. 创建客户端套接字对象  
2. 发送数据  
3. 接收数据  
4. 关闭客户端套接字  
"""  
# 导入 socket 包  
from socket import socket, SOCK_DGRAM, AF_INET  
  
# 1. 创建客户端套接字对象  
s = socket(AF_INET, SOCK_DGRAM)  
# 2. 发送数据  
# 服务器地址  
addr = ('127.0.0.1', 8888)  
s.sendto(b'hello,I am client', addr)  
# 3. 接收数据  
recv_data = s.recvfrom(1024)  
print(type(recv_data))  
print(recv_data)  
print(f'接收到远程信息{recv_data[0].decode()} from {recv_data[1]}')  
# 4. 关闭客户端套接字  
s.close()

执行结果:

说明

  1. str.encode(编码格式) 表示把字符串编码成为二进制
  2. data.decode(编码格式) 表示把二进制解码成为字符串

小结

  1. 导入 socket 模块
  2. 创建 UDP 套接字 socket
    • 参数 1:AF_INET,表示 IPv4 地址类型
    • 参数 2:SOCK_STREAM,表示 TCP 传输协议类型
  3. 发送数据 sendto
    • 参数 1:要发送的二进制数据,注意: 字符串需要使用 encode() 方法进行编码
  4. 接收数据 recvfrom
    • 参数 1:表示每次接收数据的大小,单位是字节
  5. 关闭套接字 socket 表示通信完成

10.5 UDP 持续通信开发

服务端代码

"""  
 udp 服务器持续通信代码  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 等待接收数据  
4. 发送数据  
5. 关闭套接字  
"""  
# 导入 socket 包  
from socket import socket, AF_INET, SOCK_DGRAM  
  
# 1. 创建 socket 对象  
s = socket(AF_INET, SOCK_DGRAM)  
# 2. 绑定端口  
s.bind(('127.0.0.1', 8888))  
print('等待连接……')  
# 3. 等待接收数据  
while True:  
    recv_data = s.recvfrom(1024)  # 1024 是本次接收的最大字节数  
    recv_content = recv_data[0].decode()  
    recv_address = recv_data[1]  
    print(f'接收到远程信息{recv_content} from {recv_address}')  
    if recv_content == 'exit':  
        print('server exit')  
        break  
# # 4. 发送数据  
# s.sendto(b'hello,I am server', recv_address)  
# 5. 关闭套接字  
s.close()

客户端代码

"""  
udp 客户端持续通信代码  
1. 创建客户端套接字对象  
2. 发送数据  
3. 接收数据  
4. 关闭客户端套接字  
"""  
# 导入 socket 包  
from socket import socket, SOCK_DGRAM, AF_INET  
  
# 1. 创建客户端套接字对象  
s = socket(AF_INET, SOCK_DGRAM)  
# 服务器地址  
addr = ('127.0.0.1', 8888)  
  
while True:  
    # 2. 发送数据  
    content = input('请输入:')  
    s.sendto(content.encode(), addr)  
    if content == 'exit':  
        print('client exit')  
        break  
# # 3. 接收数据  
# recv_data = s.recvfrom(1024)  
# print(f'接收到远程信息{recv_data[0].decode()} from {recv_data[1]}')  
# 4. 关闭客户端套接字  
s.close()

执行结果

客户端

服务器端

10.6 结合多线程实现 UDP 双向自由通信

注意
UDP 不同于 TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而称为服务器端。

如下服务端、客户端代码差不多,注意接收和发送端口对应,即可。该案例只实现了简单的 UDP 一对一通讯。

UDP 实现多线程服务端

# UDP实现多线程服务端  
# 导入包  
from socket import socket, AF_INET, SOCK_DGRAM  
from threading import Thread  
  
  
def recv_data():  
    flag = True  
    while True:  
        recv_data = s.recvfrom(1024)  # 1024 是本次接收的最大字节数  
        recv_content = recv_data[0].decode()  
        recv_address = recv_data[1]  
        print(f'{recv_address}消息:{recv_content}')  
        if recv_content == 'exit':  
            print('server exit')  
            break  
        elif flag:  
            flag = False  
            t2 = Thread(target=send_data, args=(recv_address,))  
            t2.daemon = True  
            t2.start()  
  
  
def send_data(addr):  
    while True:  
        # 2. 发送数据  
        content = input('请输入:')  
        s.sendto(content.encode(), addr)  
        if content == 'exit':  
            print('client exit')  
            break  
  
  
if __name__ == '__main__':  
    # 1. 创建 socket 对象  
    s = socket(AF_INET, SOCK_DGRAM)  
    # 2. 绑定端口  
    s.bind(('127.0.0.1', 8888))  
    print('等待连接')  
    t1 = Thread(target=recv_data)  
    t1.start()  
    t1.join()  
    # 关闭 socket    s.close()

UDP 实现多线程客户端

# UDP实现多线程客户端  
# 导入包  
from socket import socket, AF_INET, SOCK_DGRAM  
from threading import Thread  
  
  
def recv_data():  
    while True:  
        recv_data = s.recvfrom(1024)  # 1024 是本次接收的最大字节数  
        recv_content = recv_data[0].decode()  
        recv_address = recv_data[1]  
        print(f'{recv_address}消息:{recv_content}')  
        if recv_content == 'exit':  
            print('server exit')  
            break  
  
  
def send_data(addr):  
    flag = True  
    while True:  
        # 2. 发送数据  
        content = input('请输入:')  
        if flag and content != 'exit':  
            flag = False  
            t2 = Thread(target=recv_data)  
            t2.daemon = True  
            t2.start()  
        s.sendto(content.encode(), addr)  
        if content == 'exit':  
            print('client exit')  
            break  
  
  
if __name__ == '__main__':  
    # 1. 创建 socket 对象  
    s = socket(AF_INET, SOCK_DGRAM)  
    # 2. 绑定端口  
    s.bind(('127.0.0.1', 9999))  
    # 服务器地址  
    addr = ('127.0.0.1', 8888)  
    t1 = Thread(target=send_data, args=(addr,))  
    t1.start()  
    t1.join()  
    # 关闭 socket    s.close()

执行结果

客户端

服务器端

11 TCP

11.1 TCP 编程介绍

面向连接的 Socket 使用的协议是 TCP 协议。TCP 的 Socket 名称是 SOCK_STREAM。创建套接字 TCP 套接字,可以调用 socket.socket()。示例代码如下:

tcpSocket = socket.socket(AF_INET, SOCK_STREAM)

11.2 TCP 网络应用程序开发流程

TCP 网络应用程序开发流程的介绍

TCP 网络应用程序开发分为:

  • TCP 服务端程序开发
  • TCP 客户端程序开发

说明:

客户端程序是指运行在用户设备上的程序

服务端程序是指运行在服务器设备上的程序,专门为客户端提供数据服务。

TCP 服务端程序开发流程的介绍

步骤说明:

  1. 创建服务端端套接字对象
  2. 绑定端口号
  3. 设置监听
  4. 等待接受客户端的连接请求
  5. 接收数据
  6. 发送数据
  7. 关闭套接字

TCP 客户端程序开发流程的介绍

步骤说明:

  1. 创建客户端套接字对象
  2. 和服务端套接字建立连接
  3. 发送数据
  4. 接收数据
  5. 关闭客户端套接字

小结

  1. TCP 网络应用程序开发分为客户端程序开发服务端程序开发
  2. 主动发起建立连接请求的是客户端程序
  3. 等待接受连接请求的是服务端程序

11.3 TCP 服务端程序开发

开发 TCP 服务端程序开发步骤回顾

  1. 创建服务端端套接字对象
  2. 绑定端口号
  3. 设置监听
  4. 等待接受客户端的连接请求
  5. 接收数据
  6. 发送数据
  7. 关闭套接字

socket 类的介绍

导入 socket 模块

import socket

创建服务端 socket 对象

socket.socket(family, type)

参数说明:

  • family 表示 IP 地址类型,分为 TPv4 和 IPv6
  • type 表示传输协议类型

方法说明:

  • bind((host, port)):表示绑定端口号,host 是 ip 地址,port 是端口号,ip 地址一般不指定,表示本机的任何一个 ip 地址都可以。
  • listen(backlog):表示设置监听,backlog 参数表示最大连接的个数。
  • accept():表示等待接受客户端的连接请求
  • send(data):表示发送数据,data 是二进制数据
  • recv(bufsize):表示接收数据,bufsize 是每次接收数据的长度

TCP 服务端程序开发示例代码

"""  
TCP 最简服务器代码  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 设置监听  
4. 等待接受客户端的连接请求  
5. 接收数据  
6. 发送数据  
7. 关闭套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET  
  
# ==============1. 创建服务端端套接字对象==============  
server_socket = socket(AF_INET, SOCK_STREAM)  
# ==============2. 绑定端口号==============  
server_socket.bind(('127.0.0.1', 8888))  
# 设置端口号复用,让程序退出端口号立即释放  
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, True)  
# ==============3. 设置监听==============  
# 5:最大等待建立连接的个数  
# 提示: 目前是单任务的服务端,同一时刻只能服务于一个客户端,后续使用多任务能够让服务端同时服务于多个客户端,  
# 不需要让客户端进行等待建立连接  
# listen 后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成  
server_socket.listen(5)  
# ==============4. 等待接受客户端的连接请求==============  
# 阻塞等待客户端建立连接的请求,只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行  
# 返回值:  
# 1. 专门和客户端通信的套接字: client_socket# 2. 客户端的ip地址和端口号: client_addrclient_service_socket, client_addr = server_socket.accept()  
# 代码执行到此说明连接建立成功  
print(f"客户端的ip地址和端口号:{client_addr}")  
# ==============5. 接收数据==============  
# 接收客户端发送的数据,这次接收数据的最大字节数是1024  
recv_data = client_service_socket.recv(1024)  
print(f"收到客户端消息:{recv_data.decode()}")  
# ==============6. 发送数据==============  
send_data = "ok, 问题正在处理中...".encode()  
client_service_socket.send(send_data)  
# ==============7. 关闭套接字==============  
# 关闭服务与客户端的套接字,终止和客户端通信的服务  
client_service_socket.close()  
# 关闭服务端的套接字,终止和客户端提供建立连接请求的服务  
server_socket.close()

执行结果:

说明:

当客户端和服务端建立连接后,服务端程序退出后端口号不会立即释放,需要等待大概 1-2 分钟。

解决办法有两种:

  1. 更换服务端端口号
  2. 设置端口号复用 (推荐大家使用),也就是说让服务端程序退出后端口号立即释放。

设置端口号复用的代码如下

# 参数1: 表示当前套接字
# 参数2: 设置端口号复用选项
# 参数3: 设置端口号复用选项对应的值
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

小结

  1. 导入 socket 模块
  2. 创建 TCP 套接字 socket
    • 参数 1:AF_INET,表示 IPv4 地址类型
    • 参数 2:SOCK_STREAM,表示 TCP 传输协议类型
  3. 绑定端口号 bind
    • 参数:元组,比如:(ip 地址, 端口号)
  4. 设置监听 listen
    • 参数:最大等待建立连接的个数
  5. 等待接受客户端的连接请求 accept
  6. 发送数据 send
    • 参数:要发送的二进制数据,注意:字符串需要使用 encode() 方法进行编码
  7. 接收数据 recv
    • 参数:表示每次接收数据的大小,单位是字节,注意:解码成字符串使用 decode() 方法
  8. 关闭套接字 socket 表示通信完成

11.4 TCP 客户端程序开发

开发 TCP 客户端程序开发步骤回顾

  1. 创建客户端套接字对象
  2. 和服务端套接字建立连接
  3. 发送数据
  4. 接收数据
  5. 关闭客户端套接字

socket 类的介绍

导入 socket 模块

import socket

创建客户端 socket 对象

socket.socket(family, type)

参数说明:

  • family:表示 IP 地址类型,分为 TPv4 和 IPv6
  • type:表示传输协议类型

方法说明:

  • connect((host, port)):表示和服务端套接字建立连接,host 是服务器 ip 地址,port 是应用程序的端口号
  • send(data):表示发送数据,data 是二进制数据
  • recv(bufsize):表示接收数据,bufsize 是每次接收数据的长度

TCP 客户端程序开发示例代码

"""  
TCP最简客户端代码  
1. 创建客户端套接字对象  
2. 和服务端套接字建立连接  
3. 发送数据  
4. 接收数据  
5. 关闭客户端套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM  
  
# ==============1. 创建客户端套接字对象==============  
# 1. AF_INET:表示ipv4  
# 2. SOCK_STREAM: tcp传输协议  
client_socket = socket(AF_INET, SOCK_STREAM)  
# ==============2. 和服务端套接字建立连接==============  
client_socket.connect(('127.0.0.1', 8888))  
# 代码执行到此,说明连接建立成功  
# ==============3. 发送数据==============  
send_data = '我是客户端'.encode()  
client_socket.send(send_data)  
# ==============4. 接收数据==============  
# 接收数据,这次接收的数据最大字节数是1024  
recv_data = client_socket.recv(1024).decode()  
print(f'接收到服务器信息:{recv_data}')  
# ==============5. 关闭客户端套接字==============  
client_socket.close()

执行结果:

说明

  1. str.encode(编码格式) 表示把字符串编码成为二进制
  2. data.decode(编码格式) 表示把二进制解码成为字符串

小结

  1. 导入 socket 模块
  2. 创建 TCP 套接字 socket
    • 参数 1:AF_INET,表示 IPv4 地址类型
    • 参数 2:SOCK_STREAM,表示 TCP 传输协议类型
  3. 发送数据 send
    • 参数 1:要发送的二进制数据, 注意:字符串需要使用 encode() 方法进行编码
  4. 接收数据 recv
    • 参数 1:表示每次接收数据的大小,单位是字节
  5. 关闭套接字 socket 表示通信完成

11.5 TCP 网络应用程序的注意点

TCP 网络应用程序的注意点介绍

  1. 当 TCP 客户端程序想要和 TCP 服务端程序进行通信的时候必须要先建立连接
  2. TCP 客户端程序一般不需要绑定端口号,因为客户端是主动发起建立连接的。
  3. TCP 服务端程序必须绑定端口号,否则客户端找不到这个 TCP 服务端程序。
  4. listen 后的套接字是被动套接字,只负责接收新的客户端的连接请求,不能收发消息。
  5. 当 TCP 客户端程序和 TCP 服务端程序连接成功后, TCP 服务器端程序会产生一个新的套接字,收发客户端消息使用该套接字。
  6. 关闭 accept 返回的套接字意味着和这个客户端已经通信完毕
  7. 关闭 listen 后的套接字意味着服务端的套接字关闭了,会导致新的客户端不能连接服务端,但是之前已经接成功的客户端还能正常通信。
  8. 当客户端的套接字调用 close 后,服务器端的 recv 会解阻塞,返回的数据长度为 0,服务端可以通过返回数据的长度来判断客户端是否已经下线,反之服务端关闭套接字,客户端的 recv 也会解阻塞,返回的数据长度也为 0

11.6 TCP 双向持续通信

示例:TCP 双向通信 Socket 之服务器端

"""  
TCP 服务器持续交互代码  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 设置监听  
4. 等待接受客户端的连接请求  
5. 接收数据  
6. 发送数据  
7. 关闭套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET  
  
# ==============1. 创建服务端端套接字对象==============  
server_socket = socket(AF_INET, SOCK_STREAM)  
# ==============2. 绑定端口号==============  
server_socket.bind(('127.0.0.1', 8888))  
# 设置端口号复用,让程序退出端口号立即释放  
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, True)  
# ==============3. 设置监听==============  
# 5:最大等待建立连接的个数  
server_socket.listen(5)  
# ==============4. 等待接受客户端的连接请求==============  
print("正在等待客户端连接……")  
client_service_socket, client_addr = server_socket.accept()  
# 代码执行到此说明连接建立成功  
print(f"客户端的ip地址和端口号:{client_addr}")  
while True:  
    # ==============5. 接收数据==============  
    print("等待客户端消息……")  
    # 接收客户端发送的数据,这次接收数据的最大字节数是1024  
    recv_data = client_service_socket.recv(1024)  
    if  len(recv_data) == 0:  
        print("服务器断开连接")  
        break  
    recv_msg = recv_data.decode()  
    print(f"收到客户端消息:{recv_msg}")  
    if recv_msg == "exit":  
        print("通讯结束……")  
        break  
    # ==============6. 发送数据==============  
    msg = input("请输入: ")  
    send_data = msg.encode()  
    client_service_socket.send(send_data)  
# ==============7. 关闭套接字==============  
# 关闭服务与客户端的套接字,终止和客户端通信的服务  
client_service_socket.close()  
# 关闭服务端的套接字,终止和客户端提供建立连接请求的服务  
server_socket.close()

示例:TCP 双向通信 Socket 之客户端

"""  
TCP客户端持续交互代码  
1. 创建客户端套接字对象  
2. 和服务端套接字建立连接  
3. 发送数据  
4. 接收数据  
5. 关闭客户端套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM  
  
# ==============1. 创建客户端套接字对象==============  
# 1. AF_INET:表示ipv4  
# 2. SOCK_STREAM: tcp传输协议  
client_socket = socket(AF_INET, SOCK_STREAM)  
# ==============2. 和服务端套接字建立连接==============  
client_socket.connect(('127.0.0.1', 8888))  
while True:  
    # 代码执行到此,说明连接建立成功  
    # ==============3. 发送数据==============  
    msg = input("请输入: ")  
    send_data = msg.encode()  
    client_socket.send(send_data)  
    if msg == "exit":  
        print("通讯结束")  
        break  
    # ==============4. 接收数据==============  
    print("等待服务器消息……")  
    # 接收数据,这次接收的数据最大字节数是1024  
    recv_data = client_socket.recv(1024)  
    if len(recv_data) == 0:  
        print("服务器断开连接")  
        break  
    recv_msg = recv_data.decode()  
    print(f'接收到服务器信息:{recv_msg}')  
# ==============5. 关闭客户端套接字==============  
client_socket.close()

执行结果

客户端

服务器端

注意
运行时,要先启动服务器端,再启动客户端,才能得到正常的运行效果。
> > 上面这个程序,必须按照安排好的顺序,服务器和客户端一问一答!不够灵活!!**可以使用多线程实现更加灵活的双向通讯!!** > > 服务器端:一个线程专门发送消息,一个线程专门接收消息。 > > 客户端:一个线程专门发送消息,一个线程专门接收消息。

11.7 结合多线程实现 TCP 双向传送(自由聊天)

示例:TCP 服务端结合多线程实现自由收发信息

"""  
TCP 服务端结合多线程实现自由收发信息  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 设置监听  
4. 等待接受客户端的连接请求  
5. 接收数据  
6. 发送数据  
7. 关闭套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET  
from threading import Thread, Event  
  
  
def send_data(client_service_socket, event):  
    while True:  
        # ==============6. 发送数据==============  
        msg = input("请输入: ")  
        send_msg = msg.encode()  
        client_service_socket.send(send_msg)  
        if msg == "exit":  
            print("结束发送消息")  
            event.set()  
            break  
  
  
def recv_data(client_service_socket,event):  
    while True:  
        # ==============5. 接收数据==============  
        # 接收客户端发送的数据,这次接收数据的最大字节数是1024  
        recv_content = client_service_socket.recv(1024)  
        if len(recv_content) == 0:  
            print("====客户端断开连接====")  
            event.set()  
            break  
        recv_msg = recv_content.decode()  
        print(f"收到客户端消息:{recv_msg}")  
        if recv_msg == "exit":  
            print("通讯结束")  
            event.set()  
            break  
  
  
if __name__ == '__main__':  
    event = Event()  
    # ==============1. 创建服务端端套接字对象==============  
    server_socket = socket(AF_INET, SOCK_STREAM)  
    # ==============2. 绑定端口号==============  
    server_socket.bind(('127.0.0.1', 8888))  
    # 设置端口号复用,让程序退出端口号立即释放  
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, True)  
    # ==============3. 设置监听==============  
    # 5:最大等待建立连接的个数  
    server_socket.listen(5)  
    # ==============4. 等待接受客户端的连接请求==============  
    print("正在等待客户端连接……")  
    client_service_socket, client_addr = server_socket.accept()  
    # 代码执行到此说明连接建立成功  
    print(f"客户端的ip地址和端口号:{client_addr}")  
    t1 = Thread(target=send_data, args=(client_service_socket,event))  
    t2 = Thread(target=recv_data, args=(client_service_socket,event))  
    t1.daemon = True  
    t2.daemon = True  
    t1.start()  
    t2.start()  
    event.wait()  
    # ==============7. 关闭套接字==============  
    # 关闭服务与客户端的套接字,终止和客户端通信的服务  
    client_service_socket.close()  
    # 关闭服务端的套接字,终止和客户端提供建立连接请求的服务  
    server_socket.close()

示例:TCP 客户端结合多线程实现自由收发信息

"""  
TCP 服务端结合多线程实现自由收发信息  
1. 创建客户端套接字对象  
2. 和服务端套接字建立连接  
3. 发送数据  
4. 接收数据  
5. 关闭客户端套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM  
from threading import Thread, Event  
  
  
def send_data(client_socket, event):  
    while True:  
        # 代码执行到此,说明连接建立成功  
        # ==============3. 发送数据==============  
        msg = input("请输入: ")  
        send_msg = msg.encode()  
        client_socket.send(send_msg)  
        if msg == "exit":  
            print("结束发送消息")  
            event.set()  
            break  
  
  
def recv_data(client_socket, event):  
    while True:  
        # ==============4. 接收数据==============  
        # 接收数据,这次接收的数据最大字节数是1024  
        recv_content = client_socket.recv(1024)  
        if len(recv_content) == 0:  
            print("====服务器断开连接====")  
            event.set()  
            break  
        recv_msg = recv_content.decode()  
        print(f'接收到服务器信息:{recv_msg}')  
        if recv_msg == "exit":  
            print("通信结束")  
            event.set()  
            break  
  
  
if __name__ == '__main__':  
    # 设置一个事件控制主线程结束  
    event = Event()  
    # ==============1. 创建客户端套接字对象==============  
    # 1. AF_INET:表示ipv4  
    # 2. SOCK_STREAM: tcp传输协议  
    client_socket = socket(AF_INET, SOCK_STREAM)  
    # ==============2. 和服务端套接字建立连接==============  
    client_socket.connect(('127.0.0.1', 8888))  
    t1 = Thread(target=send_data, args=(client_socket, event))  
    t2 = Thread(target=recv_data, args=(client_socket, event))  
    t1.daemon = True  
    t2.daemon = True  
    t1.start()  
    t2.start()  
    # 阻塞等待事件的激活  
    event.wait()  
    # ==============5. 关闭客户端套接字==============  
    client_socket.close()

执行结果

客户端

服务器端

11.8 结合多线程实现 TCP 双向传送(服务器可连接多个客户端)

示例:TCP 服务端结合多线程实现自由收发信息

"""  
TCP 服务端结合多线程实现自由收发信息(可连接多个客户端)  
1. 创建服务端端套接字对象  
2. 绑定端口号  
3. 设置监听  
4. 等待接受客户端的连接请求  
5. 接收数据  
6. 发送数据  
7. 关闭套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM, SO_REUSEADDR, SOL_SOCKET  
from threading import Thread, Event  
  
  
def send_data(client_service_socket, client_addr, event):  
    while True:  
        # ==============6. 发送数据==============  
        msg = input(f"@{client_addr}> : ")  
        send_msg = msg.encode()  
        # 防止 input 阻塞等待时,socket 对象已被关闭  
        try:  
            client_service_socket.send(send_msg)  
        except OSError as e:  
            print(f"===@{client_addr}连接已断开===")  
            break  
        if msg == "exit":  
            print("结束发送消息")  
            event.set()  
            break  
  
  
def recv_data(client_service_socket, client_addr, event):  
    while True:  
        # ==============5. 接收数据==============  
        # 接收客户端发送的数据,这次接收数据的最大字节数是1024  
        recv_content = client_service_socket.recv(1024)  
        if len(recv_content) == 0:  
            print(f"===== @{client_addr} 断开连接=====")  
            event.set()  
            break  
        recv_msg = recv_content.decode()  
        print(f"收到 @{client_addr} 消息> {recv_msg}")  
        if recv_msg == "exit":  
            print(f"@{client_addr} 通信结束")  
            event.set()  
            break  
  
  
def handle_client_request(client_service_socket, client_addr):  
    event = Event()  
    t1 = Thread(target=send_data, args=(client_service_socket, client_addr, event))  
    t2 = Thread(target=recv_data, args=(client_service_socket, client_addr, event))  
    t1.daemon = True  
    t2.daemon = True  
    t1.start()  
    t2.start()  
    event.wait()  
    # 关闭服务与客户端的套接字,终止和客户端通信的服务  
    client_service_socket.close()  
  
  
if __name__ == '__main__':  
    # ==============1. 创建服务端端套接字对象==============  
    server_socket = socket(AF_INET, SOCK_STREAM)  
    # ==============2. 绑定端口号==============  
    server_socket.bind(('127.0.0.1', 8888))  
    # 设置端口号复用,让程序退出端口号立即释放  
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, True)  
    # ==============3. 设置监听==============  
    # 5:最大等待建立连接的个数  
    server_socket.listen(5)  
    while True:  
        # ==============4. 等待接受客户端的连接请求==============  
        print("正在等待新的客户端连接……")  
        client_service_socket, client_addr = server_socket.accept()  
        # 代码执行到此说明连接建立成功  
        print(f"客户端 {client_addr[0]}:{client_addr[1]} 连接成功")  
        Thread(target=handle_client_request, args=(client_service_socket, client_addr)).start()  
    # ==============7. 关闭套接字==============  
    # 服务器暂时不考虑停止  
    # 关闭服务端的套接字,终止和客户端提供建立连接请求的服务  
    # server_socket.close()

示例:TCP 客户端结合多线程实现自由收发信息

"""  
TCP 客户端结合多线程实现自由收发信息(服务器可多联)  
1. 创建客户端套接字对象  
2. 和服务端套接字建立连接  
3. 发送数据  
4. 接收数据  
5. 关闭客户端套接字  
"""  
from socket import socket, AF_INET, SOCK_STREAM  
from threading import Thread, Event, current_thread  
  
  
def send_data(client_socket, event, current_thread_name):  
    while True:  
        # 代码执行到此,说明连接建立成功  
        # ==============3. 发送数据==============  
        msg = input(f"{current_thread_name}-请输入: ")  
        send_msg = msg.encode()  
        # 防止 input 阻塞等待时,socket 对象已被关闭  
        try:  
            client_socket.send(send_msg)  
        except OSError as e:  
            print(f"==={current_thread_name}连接已断开===")  
            break  
        if msg == "exit":  
            print(f"{current_thread_name}-结束发送消息")  
            event.set()  
            break  
  
  
def recv_data(client_socket, event, current_thread_name):  
    while True:  
        # ==============4. 接收数据==============  
        # 接收数据,这次接收的数据最大字节数是1024  
        recv_content = client_socket.recv(1024)  
        if len(recv_content) == 0:  
            print(f"===={current_thread_name}-服务器断开连接====")  
            event.set()  
            break  
        recv_msg = recv_content.decode()  
        print(f'{current_thread_name}-接收到服务器信息:{recv_msg}')  
        if recv_msg == "exit":  
            print(f"{current_thread_name}-通信结束")  
            event.set()  
            break  
  
  
def main():  
    # 设置一个事件控制主线程结束  
    event = Event()  
    # ==============1. 创建客户端套接字对象==============  
    # 1. AF_INET:表示ipv4  
    # 2. SOCK_STREAM: tcp传输协议  
    client_socket = socket(AF_INET, SOCK_STREAM)  
    # ==============2. 和服务端套接字建立连接==============  
    client_socket.connect(('127.0.0.1', 8888))  
    current_thread_name = current_thread()  
    t1 = Thread(target=send_data, args=(client_socket, event, current_thread_name))  
    t2 = Thread(target=recv_data, args=(client_socket, event, current_thread_name))  
    t1.daemon = True  
    t2.daemon = True  
    t1.start()  
    t2.start()  
    # 阻塞等待事件的激活  
    event.wait()  
    # ==============5. 关闭客户端套接字==============  
    client_socket.close()  
  
  
if __name__ == '__main__':  
    for _ in range(3):  
        Thread(target=main).start()

执行结果

客户端

服务器端

11.9 socket 之 send 和 recv 原理剖析

认识 TCP socket 的发送和接收缓冲区

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

send 原理剖析

send 是不是直接把数据发给服务端?

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

recv 原理剖析

recv 是不是直接从客户端接收数据?

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

send 和 recv 原理剖析图

说明:

  • 发送数据是发送到发送缓冲区
  • 接收数据是从接收缓冲区获取

小结

不管是 recv 还是 send 都不是直接接收到对方的数据和发送数据到对方,发送数据会写入到发送缓冲区,接收数据是从接收缓冲区来读取,发送数据和接收数据最终是由操作系统控制网卡来完成。

posted @ 2026-04-11 02:02  挖掘鱼  阅读(5)  评论(0)    收藏  举报