目录
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))