目录
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.网络概念
网络就是一种辅助双方或者多方能够连接在一起的工具。
如果没有网络,可想而知单机的世界是多么孤单。
单机游戏
使用网络的目的
就是为了联通多方然后进行通信的,即把数据从乙方传递给另一方
前面的学习编写的程序都是单机的,既不能和其他电脑上的程序进行通信。
为了让在不同的电脑上运行的软件,之间能够相互传递数据,就需要借助网络的功能。
2.网络通信过程
如果两台电脑之间通过网线连接是可以直接通信的,但是需要提前设置好ip地址
并且ip地址需要控制在同一网段内,例如一台为192.168.1.1,另外一台为192.168.1.2则可以进行通信。
-
在浏览器中高输入一个网址的时候,需要将它先解析出ip地址来。
-
当得到ip地址之后,浏览器以tcp的方式3次握手连接服务器。
-
以tcp的方式发送http协议的请求数据给服务器。
-
服务器tcp的方式回应http协议的应答数据给浏览器。
TCP介绍:
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793说明(specified)。TCP在因特网协议族(TCP/IP协议族)中担任主要的传输协议,为许多应用程序(如Web浏览器和电子邮件客户端)提供可靠的数据传输服务。
TCP通信需要经过以下步骤:
-
服务器监听:服务器不断监听端口,等待客户端的连接请求。
-
客户端连接:客户端主动发起连接请求,通过“三次握手”与服务器建立连接。
-
数据传输:客户端和服务器之间可以互相发送数据。TCP通过“滑动窗口”机制实现流量控制和拥塞控制,确保数据的可靠传输。
-
关闭连接:当数据传输完成后,任何一方都可以发起关闭连接的请求。通过“四次挥手”机制,客户端和服务器关闭连接。
TCP的特点包括:
-
面向连接:TCP通信需要在传输数据之前建立连接,并在数据传输完成后关闭连接。
-
可靠传输:TCP采用确认机制、超时重传机制、流量控制机制和拥塞控制机制等措施,确保数据的可靠传输。
-
基于字节流:TCP将数据看作是一串无结构的字节流,不对数据进行任何处理。
-
全双工通信:TCP允许客户端和服务器同时发送和接收数据。
TCP的应用场景包括Web浏览器、电子邮件客户端、文件传输工具等需要可靠数据传输的应用程序。
底层细节(了解)
-
MAC地址:在设备与设备之间数据通信时来标记接受双方(网卡的序列号)
-
IP地址:在逻辑上标记一台电脑,用来指引数据包的收发方向(相当于电脑的序列号)
-
网络掩码:用来区分ip地址的网络号和主机号
-
默认网关:当需要发送的数据包的目的ip不在本网段内时,就会发送给默认的一台电脑,成为网关
-
集线器:已过时,用来连接多台电脑,缺点:每次收发数据都进行广播,网络会变的拥堵
-
交换机:集线器的升级版,有学习功能知道需要发送给哪台设备,根据需要进行单播、广播
-
路由器:连接多个不同的网段,让他们之间可以进行收发数据,每次收到数据后,ip不变,但是MAC地址会变化。
-
DNS:用来解析出IP(类似于电话簿)
-
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系列归类到四个抽象层中。
-
应用层:TFTP、HTTP、SNMP、FTP、SMTP,DNS,Telnet等等
-
传输层:TCP、UDP
-
网络层:IP、ICMP、OSPF、EIGRP、IGMP
-
数据链路层:SLIP、CSLIP、PPP、MTU
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这个样子的
2.2.网络协议栈架构
提到网络协议栈结构,最著名的当属OSI七层模型,但是TCP/IP协议族的结构稍有不同,它们之间的层次结构有如图对应关系:
可见TCP/IP被分为4层,每一层承担的任务不一样,各层的协议的工作方式也不一样,每层封装上层数据的方式也不一样:
-
应用层:应用程序通过这一层访问网络,常见的FTP、HTTP、DNS和TELNET协议;
-
传输层:TCP协议和UDP协议;
-
网络层:IP协议、ARP、RARP协议、ICMP协议等;
-
网络接口层:是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地址:
何为地址:地址就是用来标记地址的,互联网的服务:ip + port 去进行访问的
3.2.端口号
IP地址是用来发现和查找网络中的地址,但是不同程序如何互相通信呢?这就是需要端口号来识别了。如果把IP地址比作银行,端口就是出入这间房子的服务窗口。真正的银行只有几个服务窗口,但是端口采用16比特的端口号标识,一个IP地址的端口可以有65536(即:216)个之多。
服务器的默认程序一般都是通过人们所熟知的端口号来识别的。
例如,对于每个TCP/IP实现来说,
-
SMTP(简单邮件传输协议)服务器的TCP端口号都是25,
-
FTP(文件传输协议)服务器的TCP端口号都是21,
-
TFTP(简单文件传输协议)服务器的UDP端口号都是69
-
MySQL(mysql数据库)服务器的TCP端口号默认为3306
-
Redis(redis数据库)服务器默认TCP端口为6379
任何TCP/IP实现所提供的服务都用众所周知的1~1023之间的端口号。这些人们所熟知的端口号有Internet端口号分配机构(Internet Assigned Numbers Authority,IANA)来管理。
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地址。
例如:
关于域名与IP地址的映射关系,以及IP地址的路由和发现机制,暂不详细介绍。
4.Python网络编程
Python提供了两个级别访问的网络服务:
-
低级别的网络服务支持基本的Socket,它提供了标准的
BSD Socket API
,可以访问底层操作系统Socket接口的全部方法。 -
高级别的网络服务模块
SocketServer
,它提供了服务器中心类,可以简化网络服务器的开发。
socket是基于C/S架构的,也就是说进行socket网络编程,通常需要编写两个py文件,一个服务端,一个客户端。
-
c/s 客户端(手机应用、电脑应用、需要服务器提供服务的应用) 服务器
-
b/s 浏览器 (浏览器) 服务器
-
服务器(提供服务) web服务器(专门返回网页) 腾讯云服务器(部署写好的服务程序 物理设备)
BS和CS架构是两种常见的软件架构设计模式。BS架构(Browser/Server Architecture)是基于浏览器和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(浏览器)只是通过网络请求数据和交互操作。CS架构(Client/Server Architecture)是基于客户端和服务器之间的通信,将应用程序的逻辑和数据存储在服务器上,而客户端(终端设备)会运行一部分程序代码来处理数据和交互操作。
4.1.TCP/IP
我们一直处于网络的世界中,理所当然地认为底层的一切都是可以正常工作。现在我们需要来真正的深入底层,看看那些维持系统运转的东西到底是什么样。
因特网是基于规则的,这些规则定义了如何创建连接、交换数据、终止连接、处理超时等。这些规则被称为协议,它们分布在不同的层中。分层的目的是兼容多种实现方法。你可以在某一层中做任何想做的事情,只要遵循上一层和下一层的约定就行了。
最底层处理的是电信号,其余层都基于下面的层构建而成。在大约中间的位置是IP(因特网协议)层,这层规定了网络位置和地址的映射方法以及数据包(快)的传输方式。IP层的上一层有两个协议描述了如何在两个位置之间移动比特。
-
UDP(用户数据报协议)
这个协议被用来进行少量数据交换。一个数据报是一次发送的很少信息,就像明信片上的音符一样。
UDP信息并不需要确认,因此你永远无法确认它是否到达目的地。
-
TCP(传输控制协议)
这个协议被用来进行长时间的连接。它会发送比特流并确保他们都能按序到达并且不会重复。
4.2.socket的概念
到目前为止,我们学习了ip地址和端口号,还有tcp传输协议,为了保证数据的完整性和可靠性,我们使用tcp传输协议进行数据的传输,为了能够找到对应设备,我们需要使用ip地址,为了区别某个端口的应用程序接受数据,我们需要适应端口号,那么通信数据是如何完成传输的呢?
答案就是使用socket来完成。
socket(简称套接字)是进程之间通信的一个工具,好比现实生活中的插座,所有的家用电器想要工作都是基于插座进行,进程之间想要进行网络通信需要基于这个socket。
socket效果图
负责进程之间的网络数据传输,就好比数据的搬运工。
不夸张的说,只要跟网络相关的应用程序或者软件都是用到了socket。
进程之间网络数据的传输可以通过socket来完成,socket就是网络进程网络数据通信的工具。
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 | 原始套接字,普通的套接字无法处理 ICMP 、IGMP 等网络报文,而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函数
-
TCP发送数据时,一件利好TCP链接,所以不需要指定地址,而
UDP
是面向无链接的,每次发送都需要指定发送给谁。 -
服务器与客户端不能直接发送列表,元素、字典等待有数据类型的格式,发送的内容必须是字符串数据。
服务器端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服务器
-
创建套接字,绑定套接字到本地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协议)
-
开始监听链接
s.listen()
-
进入循环,不断接受客户端的链接请求
while True: conn, addr = s.accept()
-
接收客户端创来的数据,并且发送给对方数据
s.recv() s.sendall()
-
传输完毕之后,关闭套接字
s.close()
TCP客户端
-
创建套接字并链接至远端地址
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect()
2. 链接后发送数据和接收数据
s.sendall() s.recv()
-
传输完毕后,关闭套接字
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 - 蓝奏云
# 客户端循环发送数据
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服务器
生活中的电话机
如果想让别人能够打通咱们的电话获取相应服务的话,需要做一下几件事情:
-
买个手机
-
插上手机卡
-
设计手机为正常接听状态(即能够响铃)
-
静静的等着别人拨打
如同上面的电话机过程一样,在程序中,如果想要完成一个tcp服务器的功能,需要的流程如下:
-
socket
创建一个套接字 -
bind
绑定ip和port -
listen
使套接字变为可以被动链接 -
accept
等待客户端的链接 -
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()
9.TCP总结
TCP协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。
TCP通信需要经过 创建连接、数据传送、终止连接 三个步骤。
TCP通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话"。
9.1.tcp注意点
-
tcp服务器一般情况下都需要绑定,否则客户端找不到这个服务器
-
tcp客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的ip、port等信息就好,本地客户端可以随机
-
tcp服务器中通过listen可以将socket创建出来的主动套接字变为被动的,这是做tcp服务器时必须要做的
-
当客户端需要链接服务器时,就需要使用connect进行链接,udp是不需要链接的而是直接发送,但是tcp必须先链接,只有链接成功才能通信
-
当一个tcp客户端连接服务器时,服务器端会有1个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
-
listen后的套接字是被动套接字,用来接收新的客户端的链接请求的,而accept返回的新套接字是标记这个新客户端的
-
关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
-
关闭accept返回的套接字意味着这个客户端已经服务完毕
-
当客户端的套接字调用close后,服务器端会recv解堵塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线
9.2.TCP特点
面向连接
-
通信双方必须先建立连接
-
双方的数据传输都可以通过一个连接进行
-
完成数据交换,双方必须断开此连接,以释放系统资源。
这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序请使用UDP协议。
9.3.socket通信流程
socket是“打开一读/写一关闭”模式的实现,以使用TCP协议通讯的socket为例,其交互流程大概是这个样子的
-
服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket
-
服务器为socket绑定ip地址和端口号
-
服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开
-
客户端创建socket
-
客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket
-
服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
-
客户端连接成功,向服务器发送连接状态信息
-
服务器accept方法返回,连接成功
-
客户端向socket写入信息
-
服务器读取信息
-
客户端关闭
-
服务器端关闭
10.ChatRoom(网络聊天室)
一个 python + socket 实现的简单 Cli 版本聊天室
10.1.使用
你只需要安装python3环境即可运行脚本,项目下有两个包,一个叫做 client ,一个叫做 server。client 是客户端类的封装,server 是服务器类的封装。里面是核心代码。
这里的服务器监听 IP 默认设在本机作为演示,如果你想部署在服务器上需要自己手动更改 IP。
使用的时候需要先运行服务器程序,运行之后可以看到服务器日志:
[Server] 服务器正在运行......
接着开启客户端程序,客户端将自动连接到服务器程序,使用如下指令登录:
login '用户名'
输入该指令之后便可以开始聊天了,使用如下指令发送讯息:
send '消息'
发送之后,服务器将会自动将你的消息转发到所有在线的客户端,客户端收到消息后会自动显示,这样就完成了聊天室的功能。
10.2.功能设计
server(服务器)
服务器不参与会话,只提供服务。
服务器需求:
-
监听客户端的链接
-
监听客户端的信息
-
将信息广播给所有人
client(客户端)
需求:
-
登录到服务器
-
发送信息给所有人
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))