Python的网络编程一篇学透,使用Socket打开新世界

Python的网络编程一篇学透,使用Socket打开新世界_第1张图片

目录

1.网络概念

2.网络通信过程

2.1.TCP/IP

2.2.网络协议栈架构

3.TCP/IP介绍

3.1.ip地址

3.2.端口号

3.3.域名

4.Python网络编程

4.1.TCP/IP

4.2.socket的概念

4.3.Socket类型

4.4.Socket函数

4.5.Socket编程思想

5.客户端与服务器

5.1.tcp客户端

6.网络调试助手

7.循环收发数据

8.tcp服务器

8.1.运行流程

9.TCP总结

9.1.tcp注意点

9.2.TCP特点

9.3.socket通信流程

10.ChatRoom(网络聊天室)

10.1.使用

10.2.功能设计

10.3.数据通信格式

10.4.案例源码

11.附录


1.网络概念

网络就是一种辅助双方或者多方能够连接在一起的工具。

如果没有网络,可想而知单机的世界是多么孤单。

单机游戏

Python的网络编程一篇学透,使用Socket打开新世界_第2张图片

使用网络的目的

Python的网络编程一篇学透,使用Socket打开新世界_第3张图片

就是为了联通多方然后进行通信的,即把数据从乙方传递给另一方

前面的学习编写的程序都是单机的,既不能和其他电脑上的程序进行通信。

为了让在不同的电脑上运行的软件,之间能够相互传递数据,就需要借助网络的功能。

2.网络通信过程

如果两台电脑之间通过网线连接是可以直接通信的,但是需要提前设置好ip地址

并且ip地址需要控制在同一网段内,例如一台为192.168.1.1,另外一台为192.168.1.2则可以进行通信。

Python的网络编程一篇学透,使用Socket打开新世界_第4张图片

  1. 在浏览器中高输入一个网址的时候,需要将它先解析出ip地址来。

  2. 当得到ip地址之后,浏览器以tcp的方式3次握手连接服务器。

  3. 以tcp的方式发送http协议的请求数据给服务器。

  4. 服务器tcp的方式回应http协议的应答数据给浏览器。

TCP介绍

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793说明(specified)。TCP在因特网协议族(TCP/IP协议族)中担任主要的传输协议,为许多应用程序(如Web浏览器和电子邮件客户端)提供可靠的数据传输服务。

TCP通信需要经过以下步骤:

  1. 服务器监听:服务器不断监听端口,等待客户端的连接请求。

  2. 客户端连接:客户端主动发起连接请求,通过“三次握手”与服务器建立连接。

  3. 数据传输:客户端和服务器之间可以互相发送数据。TCP通过“滑动窗口”机制实现流量控制和拥塞控制,确保数据的可靠传输。

  4. 关闭连接:当数据传输完成后,任何一方都可以发起关闭连接的请求。通过“四次挥手”机制,客户端和服务器关闭连接。

TCP的特点包括:

  1. 面向连接:TCP通信需要在传输数据之前建立连接,并在数据传输完成后关闭连接。

  2. 可靠传输:TCP采用确认机制、超时重传机制、流量控制机制和拥塞控制机制等措施,确保数据的可靠传输。

  3. 基于字节流:TCP将数据看作是一串无结构的字节流,不对数据进行任何处理。

  4. 全双工通信:TCP允许客户端和服务器同时发送和接收数据。

TCP的应用场景包括Web浏览器、电子邮件客户端、文件传输工具等需要可靠数据传输的应用程序。

底层细节(了解)

  1. MAC地址:在设备与设备之间数据通信时来标记接受双方(网卡的序列号)

  2. IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)

  3. 网络掩码:用来区分ip地址的网络号和主机号

  4. 默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,成为网关

  5. 集线器:已过时,用来连接多台电脑,缺点:每次收发数据都进行广播,网络会变的拥堵

  6. 交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播

  7. 路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化。

  8. DNS:用来解析出IP(类似于电话簿)

  9. http服务器:提供浏览器能够访问到的数据

2.1.TCP/IP

网络通信是借助TCP/IP协议族,TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准,从字面上来看TCP/IP是TCP和IP协议的合称,但实际上TCP/IP协议是指因特网整个TCP/IP协议族。不同于ISO模型的七个分层,TCP/IP协议参考模型把所有的TCP/IP系列归类到四个抽象层中。

  1. 应用层:TFTP、HTTP、SNMP、FTP、SMTP,DNS,Telnet等等

  2. 传输层:TCP、UDP

  3. 网络层:IP、ICMP、OSPF、EIGRP、IGMP

  4. 数据链路层:SLIP、CSLIP、PPP、MTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这个样子的

Python的网络编程一篇学透,使用Socket打开新世界_第5张图片

Python的网络编程一篇学透,使用Socket打开新世界_第6张图片

2.2.网络协议栈架构

提到网络协议栈结构,最著名的当属OSI七层模型,但是TCP/IP协议族的结构稍有不同,它们之间的层次结构有如图对应关系:

Python的网络编程一篇学透,使用Socket打开新世界_第7张图片

可见TCP/IP被分为4层,每一层承担的任务不一样,各层的协议的工作方式也不一样,每层封装上层数据的方式也不一样:

  1. 应用层:应用程序通过这一层访问网络,常见的FTP、HTTP、DNS和TELNET协议;

  2. 传输层:TCP协议和UDP协议;

  3. 网络层:IP协议、ARP、RARP协议、ICMP协议等;

  4. 网络接口层:是TCP/IP协议的基层,负责数据帧的发送和接收。

3.TCP/IP介绍

上世纪70年代,随着计算机技术的发展,计算机使用者意识到:要想发挥计算机更大的作用,就要将世界各地的计算机连接起来。但是简单的连接是远远不够的,因为计算机之间无法沟通。因此设计一种通用的“语言”来交流是必不可少的,这时TCP/IP协议就应运而生了。

TCP/IP(Transmission Control Protocol/Internet Protocol)是传输控制协议和网络协议的简称,它定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。

TCP/IP不是一个协议,而是一个协议族的统称,里面包括了IP协议、ICMP协议、TCP协议以及http、ftp、pop3协议等。网络中的计算机都采用这套协议族进行互联。

3.1.ip地址

网络上每一个节点都必须有一个独立的IP地址,通常使用的IP地址是一个32bit的数字,被.分成4组,例如255.255.255.255就是一个IP地址。有了IP地址,用户的计算机就可以发现并连接互联网中的另外一台计算机。

在Linux系统中,可以用ifconfig -a(windows为ipconfig)命令查看自己的IP地址:

Python的网络编程一篇学透,使用Socket打开新世界_第8张图片

何为地址:地址就是用来标记地址的,互联网的服务:ip + port 去进行访问的

3.2.端口号

IP地址是用来发现和查找网络中的地址,但是不同程序如何互相通信呢?这就是需要端口号来识别了。如果把IP地址比作银行,端口就是出入这间房子的服务窗口。真正的银行只有几个服务窗口,但是端口采用16比特的端口号标识,一个IP地址的端口可以有65536(即:216)个之多。

服务器的默认程序一般都是通过人们所熟知的端口号来识别的。

例如,对于每个TCP/IP实现来说,

  1. SMTP(简单邮件传输协议)服务器的TCP端口号都是25,

  2. FTP(文件传输协议)服务器的TCP端口号都是21,

  3. TFTP(简单文件传输协议)服务器的UDP端口号都是69

  4. MySQL(mysql数据库)服务器的TCP端口号默认为3306

  5. Redis(redis数据库)服务器默认TCP端口为6379

任何TCP/IP实现所提供的服务都用众所周知的1~1023之间的端口号。这些人们所熟知的端口号有Internet端口号分配机构(Internet Assigned Numbers Authority,IANA)来管理。

Python的网络编程一篇学透,使用Socket打开新世界_第9张图片

3.3.域名

用12位数字组成的IP地址很难记忆,在实际应用时,用户一般不需要记住IP地址,互联网给每个IP地址起了一个别名,习惯上称作域名。

域名与计算机的IP地址相对应,并把这种对应关系存储在域名服务系统DNS(Domain Name System)中,这样用户只需记住域名就可以与指定的计算机进行通信了。

常见的域名包括com、net和org三种顶级域名后缀,除此之外每个国家还有自己国家专属的域名后缀(比如我国的域名后缀为cn)。目前经常使用的域名诸如百度(www.baidu.com)、Linux组织(www.lwn.net)等等。

我们可以使用命令nslookup或者ping来查看与域名相对应的IP地址。

例如:

Python的网络编程一篇学透,使用Socket打开新世界_第10张图片

关于域名与IP地址的映射关系,以及IP地址的路由和发现机制,暂不详细介绍。

4.Python网络编程

Python提供了两个级别访问的网络服务:

  1. 低级别的网络服务支持基本的Socket,它提供了标准的BSD Socket API,可以访问底层操作系统Socket接口的全部方法。

  2. 高级别的网络服务模块SocketServer,它提供了服务器中心类,可以简化网络服务器的开发。

socket是基于C/S架构的,也就是说进行socket网络编程,通常需要编写两个py文件,一个服务端,一个客户端。

  1. c/s 客户端(手机应用、电脑应用、需要服务器提供服务的应用) 服务器

  2. b/s 浏览器 (浏览器) 服务器

  3. 服务器(提供服务) web服务器(专门返回网页) 腾讯云服务器(部署写好的服务程序 物理设备)

Python的网络编程一篇学透,使用Socket打开新世界_第11张图片

BS和CS架构是两种常见的软件架构设计模式。BS架构(Browser/Server Architecture)是基于浏览器和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(浏览器)只是通过网络请求数据和交互操作。CS架构(Client/Server Architecture)是基于客户端和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(终端设备)会运行一部分程序代码来处理数据和交互操作。

Python的网络编程一篇学透,使用Socket打开新世界_第12张图片

4.1.TCP/IP

我们一直处于网络的世界中,理所当然地认为底层的一切都是可以正常工作。现在我们需要来真正的深入底层,看看那些维持系统运转的东西到底是什么样。

因特网是基于规则的,这些规则定义了如何创建连接、交换数据、终止连接、处理超时等。这些规则被称为协议,它们分布在不同的层中。分层的目的是兼容多种实现方法。你可以在某一层中做任何想做的事情,只要遵循上一层和下一层的约定就行了。

最底层处理的是电信号,其余层都基于下面的层构建而成。在大约中间的位置是IP(因特网协议)层,这层规定了网络位置和地址的映射方法以及数据包(快)的传输方式。IP层的上一层有两个协议描述了如何在两个位置之间移动比特。

  1. UDP(用户数据报协议)

    这个协议被用来进行少量数据交换。一个数据报是一次发送的很少信息,就像明信片上的音符一样。

    UDP信息并不需要确认,因此你永远无法确认它是否到达目的地。

  2. TCP(传输控制协议)

    这个协议被用来进行长时间的连接。它会发送比特流并确保他们都能按序到达并且不会重复。

4.2.socket的概念

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

答案就是使用socket来完成。

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

socket效果图

Python的网络编程一篇学透,使用Socket打开新世界_第13张图片

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

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

Python的网络编程一篇学透,使用Socket打开新世界_第14张图片

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

Python的网络编程一篇学透,使用Socket打开新世界_第15张图片

4.3.Socket类型

套接字格式:socket(family, type[, protocal])使用给定的套接族,套接字类型,协议编号(默认为0)来创建套接字。

socket 类型 描述
socket.AF_UNIX 用于同一台机器上的进程通信(既本机通信)
socket.AF_INET 用于服务器与服务器之间的网络通信
socket.AF_INET6 基于 IPV6 方式的服务器与服务器之间的网络通信
socket.SOCK_STREAM 基于TCP的流式socket通信
socket.SOCK_DGRAM 基于 UDP 的数据报式 socket 通信
socket.SOCK_RAW 原始套接字,普通的套接字无法处理 ICMPIGMP 等网络报文,而SOCK_RAW可以;其次SOCK_RAW也可以处理特殊的 IPV4 报文;此外,利用原始套接字,可以通过 IP_HDRINCL 套接字选项由用户构造 IP
socket.SOCK_SEQPACKET 可靠的连续数据包服务

创建TCP Socket

sock = socket.socket(socket.AF_INET, socket.SOOCK_SOCK_STREAM)

创建UDP Socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

4.4.Socket函数

  1. TCP发送数据时,一件利好TCP链接,所以不需要指定地址,而UDP是面向无链接的,每次发送都需要指定发送给谁。

  2. 服务器与客户端不能直接发送列表,元素、字典等待有数据类型的格式,发送的内容必须是字符串数据。

服务器端Socket函数

Socket 函数 描述
s.bind(address) 将套接字绑定到地址,在 AF_INET 下,以 tuple(host, port) 的方式传入,如s.bind((host, port))
s.listen(backlog) 开始监听TCP传入连接,backlog指定在拒绝链接前,操作系统可以挂起的最大连接数,该值最少为1,大部分应用程序设为5就够用了
s.accept() 接受TCP链接并返回(conn, address),其中conn是新的套接字对象,可以用来接收和发送数据,address是链接客户端的地址。

客户端Socket函数

Socket 函数 描述
s.connect(address) 链接到address处的套接字,一般address的格式为tuple(host, port),如果链接出错,则返回 socket.error 错误
s.connect_ex(address) 功能与 s.connect(address) 相同,但成功返回0,失败返回 errno 的值

公共的Socket函数

Socket 函数 描述
s.recv(bufsize[, flag]) 接受TCP套接字的数据,数据以字符串形式返回,buffsize 指定要接受的最大数据量,flag提供有关消息的其他信息,通常可以忽略
s.send(string[, flag]) 发送TCP数据,将字符串中的数据发送到链接的套接字,返回值是要发送的字节数量,该数量可能小于string的字节大小
s.sendall(string[, flag]) 完整发送TCP数据,将字符串中的数据发送到链接的套接字,但在返回之前尝试发送所有数据。成功返回None,失败则抛出异常
s.recvfrom(bufsize[, flag]) 接受 UDP 套接字的数据u,与 recv() 类似,但返回值是tuple(data, address)。其中data是包含接受数据的字符串,address是发送数据的套接字地址
s.sendto(string[, flag], address) 发送 UDP 数据,将数据发送到套接字,address形式为 tuple(ipaddr, port) ,指定远程地址发送,返回值是发送的字节数
s.close() 关闭套接字
s.getpeername() 返回套接字的远程地址,返回值通常是一个 tuple(ipaddr, port)
s.getsockname() 返回套接字自己的地址,返回值通常是一个 tuple(ipaddr, port)
s.setsockopt(level, optname, value) 设置给定套接字选项的值
s.getsockopt(level, optname[, buflen]) 返回套接字选项的值
s.settimeout(timeout) 设置套接字操作的超时时间,timeout是一个浮点数,单位是秒,值为None则表示永远不会超时。一般超时期应在刚创建套接字时设置,因为他们可能用于连接的操作,如 s.connect()
s.gettimeout() 返回当前超时值,单位是秒,如果没有设置超时则返回None
s.fileno() 返回套接字的文件描述
s.setblocking(flag) 如果flag为0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或send()调用无法立即发送数据,那么将引起 socket.error 异常。
s.makefile() 创建一个与该套接字相关的文件

4.5.Socket编程思想

TCP服务器

  1. 创建套接字,绑定套接字到本地ip与端口

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind()

函数socket.socket闯将一个socket,该函数带有两个参数:

Address Family:可以选择AF_INET(用于Internet进程间通信)或者AF_UNIX(用于同一台机器进行间通信),实际工作中常用AF_INET

Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要同意UDP协议)

  1. 开始监听链接

s.listen()
  1. 进入循环,不断接受客户端的链接请求

while True:
    conn, addr = s.accept()
  1. 接收客户端创来的数据,并且发送给对方数据

s.recv()
s.sendall()
  1. 传输完毕之后,关闭套接字

s.close()

TCP客户端

  1. 创建套接字并链接至远端地址

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect()

     2. 链接后发送数据和接收数据

s.sendall()
s.recv()
  1. 传输完毕后,关闭套接字

5.客户端与服务器

5.1.tcp客户端

所谓的服务器端:就是提供服务的一方

而客户端:就是需要被服务的一方

tcp客户端构建流程

tcp的科幻段要比服务端简单很多,如果说服务器端是需要自己买手机,插手机卡、设置铃声、等待别人打电话流程的话,那么客户端就只需要找一个电话亭,拿起电话拨打即可,流程要少很多。

示例代码:

import socket
​
# 创建socket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
​
# 目的信息
server_ip = input("请输入服务器ip:")
server_port = int(input("请输入服务器port:"))
​
# 链接服务器
tcp_client_socket.connect((server_ip, server_port))
​
# 提示用户输入数据
send_data = input("请输入要发送的数据:")
​
tcp_client_socket.send(send_data.encode("gbk"))
​
# 接收对方发送过来的数据,最大接收1024个字节
recvData = tcp_client_socket.recv(1024)
print('接收到的数据为:', recvData.decode('gbk'))
​
# 关闭套接字
tcp_client_socket.close()

6.网络调试助手

socket协议只要符合规范,就能够与任意的编程语言进行通信。接下来我们演示python如何网络调试助手进行通信。

网络调试助手下载地址: netassist5.0.3.zip - 蓝奏云

Python的网络编程一篇学透,使用Socket打开新世界_第16张图片

# 客户端循环发送数据
import socket
HOST = '192.168.1.100'
PORT = 8001
​
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
​
while True:
    cmd = input("请输入需要发送的信息:")
    s.send(cmd.encode())
    data = s.recv(1024)
    print(data)
    
    # s.close

7.循环收发数据

服务器循环接收数据

import socket
​
HOST = '192.168.1.100'
PORT = 8001
​
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)
​
print('服务器已经开启在:%s:%s' %(HOST, PORT))
print('等待客户端连接...')
​
while True:
    data = conn.recv(1024)
    print(data)
    
    conn.send("服务器已经接受到你的信息")
    
# conn.close()

客户端循环发送数据

import socket
HOST = '192.168.1.100'
PORT = 8001
​
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
​
while True:
    cmd = raw_input("请输入需要发送的信息:")
    s.send(cmd)
    data = s.recv(1024)
    print(data)
    
# s.close()

案例:零食销售系统

goods= {
    '瓜子': 4.5,
    '西瓜': 2,
    '矿泉水': 2.5,
}

请编写客户端与服务器,实现下面的逻辑。

>>> 【服务器】目前商店还有:瓜子(2.5),西瓜(2),矿泉水。请问您需要什么?
>>> 【客户端】红牛
>>> 【服务器】抱歉,您购买的商品目前没有,请选购其他的。
>>> 【客户端】西瓜
>>> 【服务器】购买成功,余额 -2
>>> 【客户端】拜拜
>>> 【客户端】欢迎下次光临

请问如果同时有多个顾客上门,现在的服务器是否能够支持?

8.tcp服务器

生活中的电话机

如果想让别人能够打通咱们的电话获取相应服务的话,需要做一下几件事情:

  1. 买个手机

  2. 插上手机卡

  3. 设计手机为正常接听状态(即能够响铃)

  4. 静静的等着别人拨打

如同上面的电话机过程一样,在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:

  1. socket创建一个套接字

  2. bind绑定ip和port

  3. listen使套接字变为可以被动链接

  4. accept等待客户端的链接

  5. recv/send接收发送数据

一个很简单的tcp服务器如下:

from socket import *
​
# 创建socket
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
​
# 本地信息
address = ('', 7788)
​
# 绑定
tcp_server_socket.bind(address)
​
# 使用socket创建的套接字默认的属性是主动的,使用listen将其变为被动的,这样就可以接受别人的链接了
tcp_server_socket.listen(128)
​
# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_socket用来为真客户端服务
# tcp_server_socket就可以省下来专门等待其他新客户端的链接
client_socket, clientAddr = tcp_server_socket.accept()
​
# 接受对方发送过来的数据
recv_data = client_socket.recv(1024) # 接受1024个字节
print('接受到的数据为:', recv_data.decode('gbk'))
​
# 发送一些数据到客户端
client_socket.send("thank you !".encode('gbk'))
​
# 关闭为这个客户端服务的套接字,只要关闭了,就意味着不但能再为这个客户端服务了,如果还需要服务,只能再次重新连接
client_socket.close()

8.1.运行流程

# 服务器循环接收数据
import socket
​
HOST = '192.168.1.100'
PORT = 8001
​
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)
​
print('服务器已经开启在: %s:%s' %(HOST, PORT))
print('等待客户端连接...')
​
while True:
    conn, addr = s.accept()
    print('客户端已经连接: ', addr)
​
    while True:
        data = conn.recv(1024)
        print(data)
​
        conn.send("服务器已经收到你的信息")
​
# conn.close()

Python的网络编程一篇学透,使用Socket打开新世界_第17张图片

9.TCP总结

TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

TCP通信需要经过 创建连接、数据传送、终止连接 三个步骤。

TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话"。

9.1.tcp注意点

  1. tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器

  2. tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机

  3. tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的

  4. 当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信

  5. 当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务

  6. listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的

  7. 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。

  8. 关闭accept返回的套接字意味着这个客户端已经服务完毕

  9. 当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线

9.2.TCP特点

面向连接

  1. 通信双方必须先建立连接

  2. 双方的数据传输都可以通过一个连接进行

  3. 完成数据交换,双方必须断开此连接,以释放系统资源。

这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。

9.3.socket通信流程

socket是“打开一读/写一关闭”模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这个样子的

Python的网络编程一篇学透,使用Socket打开新世界_第18张图片

  1. 服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket

  2. 服务器为socket绑定ip地址和端口号

  3. 服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开

  4. 客户端创建socket

  5. 客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket

  6. 服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求

  7. 客户端连接成功,向服务器发送连接状态信息

  8. 服务器accept方法返回,连接成功

  9. 客户端向socket写入信息

  10. 服务器读取信息

  11. 客户端关闭

  12. 服务器端关闭

10.ChatRoom(网络聊天室)

一个 python + socket 实现的简单 Cli 版本聊天室

10.1.使用

你只需要安装python3环境即可运行脚本,项目下有两个包,一个叫做 client ,一个叫做 server。client 是客户端类的封装,server 是服务器类的封装。里面是核心代码。

这里的服务器监听 IP 默认设在本机作为演示,如果你想部署在服务器上需要自己手动更改 IP。

使用的时候需要先运行服务器程序,运行之后可以看到服务器日志:

[Server] 服务器正在运行......

接着开启客户端程序,客户端将自动连接到服务器程序,使用如下指令登录:

login '用户名'

输入该指令之后便可以开始聊天了,使用如下指令发送讯息:

send '消息'

发送之后,服务器将会自动将你的消息转发到所有在线的客户端,客户端收到消息后会自动显示,这样就完成了聊天室的功能。

10.2.功能设计

server(服务器)

服务器不参与会话,只提供服务。

服务器需求:

  1. 监听客户端的链接

  2. 监听客户端的信息

  3. 将信息广播给所有人

client(客户端)

需求:

  1. 登录到服务器

  2. 发送信息给所有人

10.3.数据通信格式

登录

{
    "type": "login", # 请求类型
    "nickname": "zhangsan" # 用户名
}

登录结果

{
    "status": "ok", # 请求状态
    "id": 1 # 服务器分配的用户id
}

信息交互

发送信息

{
    'type': 'broadcast', # 用户发送信息类型
    'sender_id': 1, # 发送信息的用户id
    'message': 'message'  # 用户发送的信息
}

服务器广播

python

{
    'sneder_id': 1, # 发送信息的人
    'sneder_nickname': 'zhangsan', # 用户名
    'message': "hello world!" # 用户发送的信息
}

10.4.案例源码

服务器

import socket
import threading
import json
​
​
class Server:
    """服务器类"""
​
    def __init__(self):
        """构造"""
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 链接列表
        self.connections = list()
        # 称呼列表
        self.nicknames = list()
​
        def user_thread(self, user_id):
            """
            用户子线程
            :param user_id: 用户id
            """
​
        # 获取用户名字
        nickname = self.nicknames[user_id]
        print("[Server] 用户", user_id, nickname, "加入聊天室")
        # 广播一条信息
        self.broadcast(
            message="用户 " + str(nickname) + "(" + str(user_id) + ")" + "加入聊天室"
        )
​
        # 侦听用户发来的信息
        while True:
            try:
                buffer = connection.recv(1024).decode()
                # 解析成json数据
                obj = json.loads(buffer)
                # 如果是广播指令
                if obj["type"] == "broadcast":
                    self.broadcast(obj["sender_id"], obj["message"])
                else:
                    print(
                        "[Server] 无法解析json数据包:",
                        connection.getsockname(),
                        connection.fileno(),
                    )
            except Exception as e:
                print("[Server] 连接失效:", connection.getsockname(), connection.fileno())
                self.connections[user_id].close()
                self.connections[user_id] = None
                self.nicknames[user_id] = None
​
            def broadcast(self, user_id=0, message=""):
                """
                广播
                :param user_id: 用户id(0为系统)
                :param message: 广播内容
                """
                for i in range(1, len(self.connections)):
                    if user_id != i:
                        self.connections[i].send(
                            json.dumps(
                                {
                                    "sender_id": user_id,
                                    "sender_nickname": self.nicknames[user_id],
                                    "message": message,
                                }
                            ).encode()
                        )
​
            def start(self, address):
                """
                启动服务器
                """
                # 绑定端口
                self.socket.bind(address)
                # 启用监听
                self.socket.listen(10)
                print("[Server] 服务器正在运行......")
​
                # 清空连接
                self.connections.clear()
                self.nicknames.clear()
                # 添加管理员账号
                self.connections.append(None)
                self.nicknames.append("System")
​
                # 开始侦听
                while True:
                    # 接收连接
                    connection, address = self.socket.accept()
                    print("[Server] 收到一个新连接", connection.getsockname(), connection.fileno())
                    # 开启新的线程,尝试接受数据
                    threading.Thread(
                        target=self.handle_login, args=(connection,), daemon=True
                    ).start()
​
            def handle_login(self, connection):
                # 尝试接受数据
                try:
                    buffer = connection.recv(1024).decode()
                    # 解析成 json 数据
                    obj = json.loads(buffer)
                    # 如果是连接指令,那么则返回一个新的用户编号,接收用户连接
                    if obj["type"] == "login":
                        self.connections.append(connection)
                        self.nicknames.append(obj["nickname"])
​
                        # 返回 json {'id':编号}
                        connection.send(json.dumps({"id": len(self.connections) - 1}).encode())
​
                        # 开辟一个新的线程
                        # 如果主线程结束,其他线程一起结束
                        thread = threading.Thread(
                            target=self.user_thread, args=(len(self.connections) - 1,)
                        )
                        thread.daemon = True
                        thread.start()
                    else:
                        print(
                            "[Server] 无法解析json数据包:",
                            connection.getsockname(),
                            connection.fileno(),
                        )
                except Exception as e:
                    print(e)
                    print("[Server] 无法接受数据:", connection.getsockname(), connection.fileno())
​
        if __name__ == "__main__":
            server = Server()
            server.start(("0.0.0.0", 8000))

客户端

import socket
import threading
import json
​
"""
定义一个客户端类,
    属性:socket、id、name
​
    行为:
        启动客户端
        帮助信息
        登录
        发送信息
        接收信息
"""
​
​
class Client:
    """
    客户端
    """
​
    prompt = ""
    intro = (
        "[Welcome] 简易聊天室客户端(Cli版)\n"
        + "[Help] login nickname - 登录到聊天室,nickname是你选择的昵称\n"
        + "[Help] send message - 发送消息,message是你输入的消息"
    )
​
    def __init__(self):
        """
        构造
        """
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.id = None
        self.nickname = None
​
    def receive_message_thread(self):
        """接受消息线程"""
        while True:
            # noinspection PyBroadException
            try:
                buffer = self.socket.recv(1024).decode()
                obj = json.loads(buffer)
                # print(obj)
                print(
                    "["
                    + str(obj["sender_nickname"])
                    + "("
                    + str(obj["sender_id"])
                    + ")"
                    + "]",
                    obj["message"],
                )
            except Exception:
                print("[Client] 无法从服务器获取数据")
​
    def send_message_thread(self, message):
        """发送消息线程"""
        self.socket.send(
            json.dumps(
                {"type": "broadcast", "sender_id": self.id, "message": message}
            ).encode()
        )
​
    def start(self, address):
        """启动客户端"""
        self.socket.connect(address)
        print(self.intro)
        while True:
            action = input("").strip()
            if action.lower().startswith("login"):
                self.do_login(action)
            elif action.lower().startswith("send"):
                self.do_send(action)
            else:
                print("[Help] login nickname - 登录到聊天室,nickname是你选择的昵称")
                print("[Help] send message - 发送消息,message是你输入的消息")
​
    def do_login(self, args):
        """登录聊天室"""
        nickname = args.split()[1]
​
        # 将昵称发送给服务器,获取用户id
        self.socket.send(json.dumps({"type": "login", "nickname": nickname}).encode())
        # 尝试接受数据
        # noinspection PyBroadException
        try:
            buffer = self.socket.recv(1024).decode()
            obj = json.loads(buffer)
            if obj["id"]:
                self.nickname = nickname
                self.id = obj["id"]
                print("[Client] 成功登录到聊天室")
​
                # 开启子线程用于接受数据
                thread = threading.Thread(target=self.receive_message_thread)
                thread.daemon = True
                thread.start()
            else:
                print("[Client] 无法登录到聊天室")
        except Exception:
            print("[Client] 无法从服务器获取数据")
​
    def do_send(self, args):
        """
        发送消息
        :param args: 参数
        """
        message = args[5:]
        # 显示自己发送的消息
        print("[" + str(self.nickname) + "(" + str(self.id) + ")" + "]", message)
        # 开启子线程用于发送数据
        thread = threading.Thread(target=self.send_message_thread, args=(message,))
        thread.daemon = True
        thread.start()
​
​
if __name__ == "__main__":
    client = Client()
    client.start(("127.0.0.1", 8000))

11.附录

你可能感兴趣的:(python高阶学习,php,开发语言,python,网络,网络编程,socket,tcp)