Linux相关命令
- ifconfig:查看网卡信息
- sudo ifconfig ens33 ip:修改网卡ens33的ip地址
- ping ip/域名:检测网络是否正常
- route -n:查看路由
- netstat -an:查看端口状态
- sudo lsof -i [tcp/udp]:端口号:查看哪一个进程用了该端口
- ps -elf|grep udp_server:查看某个进程是否还在
-
Socket简介
socket(套接字)是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信
- 套接字的使用流程
- 创建套接字
- 使用套接字收 / 发数据
- 关闭套接字
- 创建socket
- AddressFamily:可以选择AF_INET(用于Internet进程间通信)或者AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET
- Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)
- 服务器端套接字的方法
- s.bind():绑定地址(host, port)到套接字,在AF_INET下,以元组的形式表示地址
- s.listen():开始TCP监听,backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量,该值至少为1,大部分应用程序设为5就可以了
- s.accept():被动接受TCP客户端连接,阻塞式等待连接的到来
- 客户端套接字的方法
- s.connect():主动初始化TCP服务器连接,一般地址的格式为元组(host, port),如果连接出错,返回socket.error异常
- s.connect_ex():connect()方法的扩展版本,出错时返回出错码,而不是抛出异常
- 共用的方法
- s.send():发送TCP数据,将string中的数据发送到连接的套接字,返回值是要发送的字节数,该数值可能小于string的字节大小
- s.sendall():完整发送TCP数据,将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据,成功返回None,失败则抛出异常
- s.recv():接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量,flag提供有关消息的其他信息,通常可以忽略
- s.sendto():发送UDP数据,返回值是发送的字节数
- s.recvfrom():接收UDP数据,返回值是(data, socket),其中data是包含接收数据的字符串,socket是发送端的套接字
- s.close():关闭套接字
- 其他:python网络编程
UDP通信
User Datagram Protocol:用户数据报协议通信结构图
编程说明
- 服务器端的ip地址必须是本机地址,否则会报错
- 服务器端的端口号需要指定
- 说明:端口号只有整数,范围是0~65535,知名端口的范围是0~1023,动态端口的范围是1024~65535,动态端口表示它一般不固定分配给某种服务
- 举例
- 客户端的端口号可以不用指定,每次连接系统默认会随机分配,ip地址也不需要指定
- 发送数据需要进行编码转换
- encode编码:str —> bytes,例如 ‘helloworld’.encode(‘utf8’)
- decode解码:bytes —> str,例如 recv_data[0].decode(‘utf8’)
- recvfrom()方法中的参数表示接收的最大字节数,如果该数小于另一端发送的字节数,Windows会报错,Linux不会报错,接收不下的数据直接丢弃
举例
```python import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_addr = (‘127.0.0.1’, 2000)
client_socket.sendto(‘helloworld’.encode(‘utf8’), server_addr)
recv_data = client_socket.recvfrom(50) print(recv_data[0].decode(‘utf8’))
client_socket.close()
```python
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
recv_data = server_socket.recvfrom(50)
print(recv_data[0].decode('utf8')) # 显示接收到的数据
print(recv_data[1]) # 显示对方的ip地址和端口号
server_socket.sendto('Ilovepython'.encode('utf8'), recv_data[1])
server_socket.close()
TCP通信
Transmission Control Protocol:传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP通信需要经过创建连接、传送数据、终止连接三个步骤
特点
- 面向连接
- 通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输
- 双方间的数据传输都可以通过这一个连接进行
- 完成数据交换后,双方必须断开此连接,以释放系统资源
- 这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序需要使用UDP协议
可靠传输
- TCP采用发送应答机制:每一个报文都有一个序列号,TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
- 超时重传:发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段
- 错误检验:TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和
- 流量控制和阻塞管理:使用滑动窗口机制来控制双方的发送接收速度,用于避免主机发送得过快而使接收方来不及完全收下
TCP和UDP的区别
三次握手
关于三次握手和四次挥手的常见面试题
四次挥手
长连接和短连接
一种短连接的情况
- client向server发起连接请求
- server接到请求,双方建立连接
- client向server发送信息
- server回应client
- 一次读写完成后,双方任何一个都可以发起close操作
- 说明:一般都是client先发起close操作,短连接一般只会传递一次读写操作
- 一种长连接的情况
- client向server发起连接请求
- server接到请求,双方建立连接
- client向server发送消息
- server回应client
- 一次读写完成,连接不关闭
- 后续读写操作
- 长时间操作之后,client发起关闭请求
- 长连接和短连接的对比
- 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间,对于频繁请求资源的客户来说,较适用长连接
- client和server之间的连接如果一直不关闭的话,会存在一个问题:随着客户端连接越来越多,server早晚有扛不住的时候,这时候server需要采取一些策略,比如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损。如果条件再允许可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样就可以完全避免某个客户端连累后端服务
- 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段,但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽
长连接和短连接的应用场景
tcp服务器需要绑定,否则客户端找不到这个服务器
- tcp客户端不需要绑定,因为是主动连接服务器,所以只要确定好服务器的ip、port等信息就好
- tcp服务器通过listen可以将socket创建出来的主动套接字变为被动套接字
- 当客户端需要连接服务器时,就需要使用connect进行连接,udp是不需要连接而是直接发送,但是tcp必须先连接,只有连接成功才能通信
- 当一个tcp客户端连接服务器时,服务器端会有一个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
- listen后的套接字是被动套接字,用来接收新的客户端的连接请求,而accept返回的新套接字是标记这个新客户端的
- 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够连接服务器,但是之前已经连接成功的客户端正常通信
- 关闭accept返回的套接字意味着这个客户端已经服务完毕
- 当客户端的套接字调用close后,服务器端会解阻塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来判断客户端是否已经下线
举例
```python import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_addr = (‘127.0.0.1’, 2000) client_socket.connect(server_addr) # 连接服务器
client_socket.send(b’hello, world, I am ZYJ’) # 发送数据
recv_data = client_socket.recv(100) # 接收数据 print(recv_data)
client_socket.close()
```python
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10) # server_socket对象的缓冲大小
# 如果有新的客户端来连接服务器,那么就产生一个新的套接字专门为这个客户端服务
client_socket, client_addr = server_socket.accept()
recv_data = client_socket.recv(5)
print(recv_data)
recv_data = client_socket.recv(10)
print(recv_data)
client_socket.send(b'study')
client_socket.close() # 关闭为这个客户端服务的套接字
server_socket.close() # 关闭服务器
非阻塞编程
正常的recv、accept会阻塞,可以通过socket.setblocking(False)实现非阻塞编程
import socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
client_socket.connect(server_addr) # 连接服务器
client_socket.send(b'hello, I am ZYJ') # 发送数据
client_socket.close()
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用对应地址和端口
server_socket.setblocking(False) # 设置server_socket为非阻塞
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(100)
client_socket = None
while True:
try:
client_socket, client_addr = server_socket.accept()
except Exception as e:
pass
if client_socket:
client_socket.setblocking(False) # 设置client_socket为非阻塞
try:
recv_data = client_socket.recv(1024)
if not recv_data: # 如果客户端断开
print('bye')
exit()
print(recv_data.decode('utf-8'))
except Exception as e:
pass
多路复用编程:epoll使用
epoll是对select、poll模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的C/S架构
相关用法
- import select:导入select模块
- epoll = select.epoll():创建一个epoll对象
- epoll.register(文件句柄,事件类型):注册要监控的文件句柄和事件
- 事件类型
- select.EPOLLIN:可读事件
- select.EPOLLOUT:可写事件
- select.EPOLLERR:错误事件
- select.EPOLLHUP:客户端断开事件
- epoll.unregister(文件句柄):销毁文件句柄
- epoll.poll(timeout):当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout为超时时间,默认为 -1,即一直等待直到文件句柄发生变化。如果指定为1,那么epoll每一秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
- epoll.fileno():返回epoll的控制文件描述符
- epoll.modify(fineno, envent):fineno为文件描述符,event为文件类型,该语句的作用是修改文件描述符所对应的事件
- epoll.fromfd(fileno):从一个指定的文件描述符创建一个epoll对象
- epoll.close():关闭epoll对象的控制文件描述符
双方即时聊天
```python import socket import select import sys
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_addr = (‘127.0.0.1’, 2000) client_socket.connect(server_addr)
epoll = select.epoll() # 创建一个epoll对象 epoll.register(sys.stdin.fileno(), select.EPOLLIN) # 监控标准输入缓冲区 epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控客户端的接收缓冲区
while True: epoll_events = epoll.poll(-1, 2) # -1表示永久等待,2表示监控了两个描述符 for fd, event in epoll_events: if fd == sys.stdin.fileno(): # 标准输入缓冲区里边有数据 send_data = input() client_socket.send(send_data.encode(‘utf8’)) elif fd == client_socket.fileno(): # 客户端的接收缓冲区里边有数据 recv_data = client_socket.recv(100) if not recv_data: print(‘服务器断开了’) exit() print(recv_data.decode(‘utf8’))
client_socket.close()
```python
import socket
import select
import sys
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)
client_socket, client_addr = server_socket.accept()
epoll = select.epoll() # 创建一个epoll对象
epoll.register(sys.stdin.fileno(), select.EPOLLIN) # 监控标准输入缓冲区
epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控接收客户端的缓冲区
while True:
events = epoll.poll(-1, 2) # -1表示永久等待,2表示监控了两个描述符
for fd, event in events:
if fd == sys.stdin.fileno(): # 标准输入缓冲区里边有数据
data = input()
client_socket.send(data.encode('utf8'))
elif fd == client_socket.fileno(): # 接收客户端的缓冲区里边有数据
recv_data = client_socket.recv(100)
if not recv_data:
print('客户端断开了')
exit()
print(recv_data.decode('utf8'))
client_socket.close()
server_socket.close()
客户端断开重连
客户端断开时,内核把socket对象对应的描述符标记为可读状态,epoll就会检测到,这时候服务器读到的内容为空,可以通过这个来判断,从而停止监测客户端
import socket
import select
import sys
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用对应地址和端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)
epoll = select.epoll() # 创建一个epoll对象
epoll.register(sys.stdin.fileno(), select.EPOLLIN) # 监控标准输入缓冲区
epoll.register(server_socket.fileno(), select.EPOLLIN) # 监控服务器端的接收缓冲区
client_socket = None
while True:
epoll_events = epoll.poll(-1, 3)
for fd, event in epoll_events:
if fd == server_socket.fileno(): # 服务器被客户端连接了
print('客户端连接了')
client_socket, _ = server_socket.accept()
epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控接收客户端的缓冲区
elif fd == sys.stdin.fileno(): # 标准输入缓冲区里边有数据
send_data = input()
if not client_socket:
print('客户端尚未连接')
continue
client_socket.send(send_data.encode('utf8'))
elif fd == client_socket.fileno(): # 接收客户端的缓冲区里边有数据
recv_data = client_socket.recv(100)
if not recv_data:
print('客户端断开了')
epoll.unregister(client_socket.fileno())
client_socket.close()
continue
print(recv_data.decode('utf8'))
client_socket.close()
server_socket.close()
多人聊天室
import socket
import select
import sys
if len(sys.argv) != 2:
print('input as the format: python3 char_room_client.py IP')
exit(2)
name = input("please input your name: ")
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = (sys.argv[1], 2000)
client_socket.connect(server_addr)
epoll = select.epoll() # 创建一个epoll对象
epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控标准输入缓冲区
epoll.register(sys.stdin.fileno(), select.EPOLLIN) # 监控客户端的接收缓冲区
while True:
epoll_events = epoll.poll()
for fd, event in epoll_events:
if fd == sys.stdin.fileno(): # 标准输入缓冲区里边有数据
try: # Ctrl+D退出
send_data = input()
except:
print('I will leave')
client_socket.close()
exit(2)
send_data = name + ":" + send_data
client_socket.send(send_data.encode('utf-8'))
if fd == client_socket.fileno(): # 客户端的接收缓冲区里边有数据
recv_data = client_socket.recv(100)
if not recv_data:
exit(0)
print(recv_data.decode('utf-8'))
client_socket.close()
import socket
import select
import sys
if len(sys.argv) != 2:
print('input as the format: python3 char_room_server.py IP')
exit(2)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用对应地址和端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_addr = (sys.argv[1], 2000)
server_socket.bind(server_addr)
server_socket.listen(10)
epoll = select.epoll() # 创建一个epoll对象
epoll.register(server_socket.fileno(), select.EPOLLIN) # 监控服务器端的接收缓冲区
client_dict = {}
client_socket = None
while True:
epoll_events = epoll.poll()
for fd, event in epoll_events:
if fd == server_socket.fileno(): # 服务器被该客户端连接了
client_socket, client_addr = server_socket.accept()
print("{} is coming".format(client_addr))
epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控接收该客户端的缓冲区
client_dict[client_socket.fileno()] = client_socket
else: # 服务器的对应该客户端的接收缓冲区接收到数据
recv_data = client_dict[fd].recv(10)
if recv_data:
for fileno, client in client_dict.items():
if fd != fileno:
client.send(recv_data)
else: # 对应该客户端的接收缓冲区里接收到的数据为空,说明该客户端断开了
print('{} is leaving'.format(client_dict[fd]))
epoll.unregister(fd)
client_dict[fd].close()
client_dict.pop(fd)
client_socket.close()
server_socket.close()
持续发送多个文件
方法:发送文件名的长度 —-> 发送文件名 —-> 发送文件内容的长度 —-> 发送文件内容
import socket
import struct
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
client_socket.connect(server_addr)
file_name_len = client_socket.recv(4) # 接收文件名的长度
file_name = client_socket.recv(struct.unpack('I', file_name_len)[0]) # 接收文件名
file = open(file_name.decode('utf8'), 'wb')
file_content_len = client_socket.recv(4) # 接收文件内容的长度
file_content = client_socket.recv(struct.unpack('I', file_content_len)[0]) # 接收文件内容
file.write(file_content) # 将文件内容写入文件
file.close()
client_socket.close()
import socket
import struct
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用对应地址和端口
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)
client_socket, client_addr = server_socket.accept()
file_name = 'Readme'
file_name_bytes = file_name.encode('utf8')
client_socket.send(struct.pack('I', len(file_name_bytes))) # 发送文件名的长度
client_socket.send(file_name_bytes) # 发送文件名
file = open(file_name, 'rb')
file_content_bytes = file.read()
client_socket.send(struct.pack('I', len(file_content_bytes))) # 发送文件内容的长度
client_socket.send(file_content_bytes) # 发送文件内容
file.close()
client_socket.close()
server_socket.close()