socket案例
使用socket写一个C/S架构程序
基于TCP的聊天程序
TCP是基于链接的,必须先启动服务端,再启动客户端,让客户端去链接服务端。
服务端代码:
import socket
# 初始化一个socket对象
s_obj = socket.socket(socket.AF_INET, # 表示协议簇,用于socket()的第一个参数.
socket.SOCK_STREAM) # 表示使用TCP协议, TCP协议也称为流式协议.
# 绑定本地IP和端口,用一个二元组
s_obj.bind(('127.0.0.1',8080))
# 开始监听,参数为半链接池大小.
s_obj.listen(3)
# 接收客户端的连接,返回值是一个 (conn, address) 元组,
# 其中conn 是一个已连接的套接字对象,用于在此连接上收发数据,
# address 是连接另一端的套接字所绑定的IP地址.
conn,ip_addr = s_obj.accept()
# 查看对方的IP地址和端口
print(f'对方IP地址: {ip_addr}')
# 接收对方数据,指定每次接收的字节数
data = conn.recv(1024)
print(data.decode('utf-8')) # 网络传输的是bytes类型
# 给对方发送信息
conn.send('服务端发送给对方的信息'.encode('utf-8'))
# 关闭链接对象
conn.close()
# 关闭套接字对象
s_obj.close()
客户端代码:
import socket
# 初始化一个socket对象
s_obj = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 客户端不用绑定地址,直接连接服务端即可,如果是TCP协议,connect和服务端的accept会进行三次握手
s_obj.connect(('127.0.0.1', 8080)) # 指定服务端IP和端口
# 链接成功后就可以通信.
# 服务端是先接收再发送,对应着客户端则先发送再接收
s_obj.send('客户端发送的数据'.encode('utf-8')) # 都要使用相同的编码
# 同样指定接收的字节数
data = s_obj.recv(1024)
print(data.decode('utf-8'))
# 关闭socket对象
s_obj.close()
上面只能实现一次通信,并且有些bug:
- 1、当发送的数据为空时接收端
recv()
会一直处于收空状态,不会去执行下一步代码,而发送端还在等待接收端返回数据才能进入下一步操作,这就进入了死循环,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住。 - 2、在Windows上,当客户端突然断开链接时服务端
recv()
会抛异常;而在Linux中,recv()
会不断收空。
针对问题1我们可以在发送端进行限制,问题2在Windows上可以使用try异常处理来捕获异常断开本次连接,在Linux上使用一个if判断来结束连接,再通过while循环实现多次链接和多次通信。
改进版服务端代码:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(3)
# 链接循环
while 1:
print('等待客户端链接...')
conn,addr = s.accept()
print(f'客户端 {addr} 已连接!')
# 通信循环
while 1:
try:
# 检测异常
data = conn.recv(1024)
# Linux异常
if not data:
print(f'客户端{addr}已断开!')
break
print(data.decode('utf-8'))
# 发送数据
while 1:
msg = input('输入发送给客户端的信息: ').strip()
if not msg:
print('不能发送空消息!')
continue
break
conn.send(msg.encode('utf-8'))
# 捕获到异常结束收发消息的循环,断开连接
except Exception:
print(f'{addr} 已断开连接!')
break
conn.close() # 关闭连接对象,重新等待新的客户端连接.
# s.close() 服务端通常会一直保持监听状态
改进版客户端代码:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))
while 1:
msg = input('输入发送至服务端的信息(q断开连接): ').strip()
if msg.lower() == 'q':
s.close()
print('已断开...')
break
if not msg:
print('不能发送空消息!')
continue
s.send(msg.encode())
data = s.recv(1024)
print(data.decode())
重启server常见问题
一、在POSIX系统中重启server可能会出现端口被占用的情况,解决方式有三种:
- 1、修改绑定的端口。
- 2、使用
netstat
命令查看占用该端口的进程,kill
掉该进程即可。 - 3、在实例化socket对象的下面添加一行代码。
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 在下面添加一行,重用ip和端口
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
二、在bind()
方法中出现Address already in use
这个错误。
这是由于我们虽然关闭了连接对象和socket对象,但服务端仍处于time_wait
状态仍在占用地址。
解决方法也可使用上面的方法三,第二种是修改Linux系统内核参数。
# 发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf
# 编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
# 然后执行 /sbin/sysctl -p 让参数生效。
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量
SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
基于UDP的聊天程序
UDP是无链接的,先启动哪一端都不会报错。
服务端:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(('127.0.0.1', 8080))
while 1:
msg, addr = s.recvfrom(1024)
print(f'{addr} 发送数据: {msg.decode("utf-8")}')
to_client_msg = input('输入发送信息: ').strip()
s.sendto(to_client_msg.encode('utf-8'), addr)
客户端:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
while 1:
to_server_msg = input('输入发送给服务端的信息: ').strip()
s.sendto(to_server_msg.encode('utf-8'), ('127.0.0.1', 8080))
back_msg, addr = s.recvfrom(1024)
print(f'{addr} 发送消息: {back_msg.decode("utf-8")}')
udp是基于数据报无连接的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,所以发送空数据也不会陷入收空循环,并且可以有多个客户端同时向服务端发数据。
基于UDP的QQ聊天
服务端
import socket
ip_port = ('127.0.0.1',8080)
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as s:
s.bind(ip_port)
# 收发循环
while 1:
msg,addr = s.recvfrom(1024)
print(f'来自{addr}的一条消息: {msg.decode("utf-8")}')
while 1:
back_msg = input('回复:').strip()
if back_msg:
s.sendto(back_msg.encode('utf-8'),addr)
break
客户端
import socket
user_dic = {
'李白': ('127.0.0.1', 8080),
'苏轼': ('127.0.0.1', 8080),
'杜甫': ('127.0.0.1', 8080),
}
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
while 1:
user = input('选择聊天对象:').strip()
if user in user_dic:
while 1:
msg = input('发送消息:').strip()
if msg:
s.sendto(msg.encode('utf-8'), user_dic[user])
back_msg,addr = s.recvfrom(1024)
print(back_msg.decode('utf-8'))
else:
print('对象不存在!重新选!')
时间服务器
ntp服务端
import socket
from time import strftime
ip_port = ('127.0.0.1',8080)
bufsize = 1024
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(ip_port)
print('服务端已启动。。。')
while 1:
data, addr = s.recvfrom(bufsize)
print(f"来自{addr}的{data}请求!")
s.sendto(strftime('%Y-%m-%d %X').encode('utf-8'),addr)
ntp客户端
import socket
ntp_ip_port = ('127.0.0.1',8080)
bufsize = 1024
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.sendto('get time'.encode('utf-8'),ntp_ip_port)
time, _ntp_ip_port = s.recvfrom(1024)
print(time.decode('utf-8'))
基于TCP的远程执行命令程序
使用subprocess模块。
服务端:
import socket
import subprocess
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(3)
print('服务端已启动!\n等待客户端链接中...')
while 1:
conn,addr = s.accept()
print(f'客户端 {addr} 已连接!')
while 1:
try:
cmd = conn.recv(1024)
if not cmd:
conn.close()
print(f'客户端{addr}已断开')
break
s_obj = subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = s_obj.stdout.read()
stderr = s_obj.stderr.read()
conn.send(stdout+stderr)
except Exception:
break
conn.close()
print(f'{addr} 已断开连接!')
客户端:
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8080))
while 1:
cmd = input('>>> ').strip()
if not cmd:
break
if cmd.lower() == 'q':
s.close()
s.send(cmd.encode('utf-8'))
data = s.recv(1024)
print(data.decode('gbk')) # Windows系统用GBK编码接收。
需要注意的是,subprocess执行命令的结果编码是以操作系统为准的,如果是Windows,编码为GBK。另外,一条命令可能同时既有正确输出,又有错误输出,比如ls ; abc ; ifconfig
,read()
只能从每个管道里面读取一次内容,第二次再读是没有内容的。
另外,无论是服务端还是客户端,每次接收的数据量都是1024字节,当数据在1024字节以内时可以正常接收,而当数据量大于1024字节时,就会发生黏包问题。只有TCP会发生黏包问题,UDP不会出现,而是直接将多余的数据丢弃。
关于黏包问题请看下篇。
基于UDP的远程执行命令程序
服务端:
import socket
import subprocess
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(('127.0.0.1', 8080))
# 通信循环
while 1:
cmd, addr = s.recvfrom(1024)
print(f'{addr} 已连接!')
# 执行命令
res = subprocess.Popen(cmd.decode('utf-8'), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
res1 = res.stdout.read()
res2 = res.stderr.read()
# 客户端仅recvfrom一次,所以服务端仅sendto一次
s.sendto(res1 + res2, addr)
客户端:
import socket
ip_port = ('127.0.0.1', 8080)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
while 1:
cmd = input('>>>: ').strip()
s.sendto(cmd.encode('utf-8'), ip_port)
msg, addr = s.recvfrom(1024)
print(addr)
print(msg.decode('gbk'))
UDP是数据报(datagram),每次发送的都是一个数据报,所以每个sendto()
都应该对应一个recvfrom()
,当sendto()
发送的数据量大于recvfrom()
接收的数据量时,在Linux系统中仅接收recvfrom()
指定的数据量,会丢未接收的数据,而在Windows系统上会直接报错。
OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。
解决方式请看黏包问题篇。