Python的socket与socketserver怎么使用

一、基于TCP协议的socket套接字编程1、套接字工作流程

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束,使用以下Python代码实现:

import socket
# socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0
socket.socket(socket_family, socket_type, protocal=0)
# 获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 1、 服务端套接字函数
  • s.bind():绑定(主机,端口号)到套接字

  • s.listen():开始TCP监听

  • Python网络编程:从socket到socketserver

    s.accept():被动接受TCP客户的连接,(阻塞式)等待连接的到来

2、 客户端套接字函数
  • s.connect():主动初始化TCP服务器连接

  • s.connect_ex():connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

3、 公共用途的套接字函数
  • s.recv():接收TCP数据

  • s.send():发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)

  • s.sendall():发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)

  • s.recvfrom():接收UDP数据

  • s.sendto():发送UDP数据

  • s.getpeername():连接到当前套接字的远端的地址

  • s.getsockname():当前套接字的地址

  • s.getsockopt():返回指定套接字的参数

  • s.setsockopt():设置指定套接字的参数

  • s.close():关闭套接字

4、 面向锁的套接字方法
  • s.setblocking():设置套接字的阻塞与非阻塞模式

  • s.settimeout():设置阻塞套接字操作的超时时间

  • s.gettimeout():得到阻塞套接字操作的超时时间

5、 面向文件的套接字的函数
  • s.fileno():套接字的文件描述符

  • s.makefile():创建一个与该套接字相关的文件

2、基于TCP协议的套接字编程

可以通过netstat -an | findstr 8080查看套接字状态

1、 服务端import socket
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcp称为流式协议,udp称为数据报协议SOCK_DGRAM
# print(phone)
# 2、插入/绑定手机卡
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('
127.0.0.1'
, 8080))
# 3、开机
phone.listen(5) # 半连接池,限制的是请求数
# 4、等待电话连接
print('
start....'
)
while True: # 连接循环
conn, client_addr = phone.accept() # (三次握手建立的双向连接,(客户端的ip,端口))
# print(conn)
print('
已经有一个连接建立成功'
, client_addr)
# 5、通信:收\发消息
while True: # 通信循环
try:
print('
服务端正在收数据...'
)
data = conn.recv(1024) # 最大接收的字节数,没有数据会在原地一直等待收,即发送者发送的数据量必须>
0bytes
# print('
===>
'
)
if len(data) == 0: break # 在客户端单方面断开连接,服务端才会出现收空数据的情况
print('
来自客户端的数据'
, data)
conn.send(data.upper())
except ConnectionResetError:
break
# 6、挂掉电话连接
conn.close()
# 7、关机
phone.close()
# start....
# 已经有一个连接建立成功 ('
127.0.0.1'
, 4065)
# 服务端正在收数据...
# 来自客户端的数据 b'
\xad'

# 服务端正在收数据... 2、 客户端import socket
# 1、买手机
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# print(phone)
# 2、拨电话
phone.connect(('
127.0.0.1'
, 8080)) # 指定服务端ip和端口
# 3、通信:发\收消息
while True: # 通信循环
msg = input('
>
>
: '
).strip() # msg='
'

if len(msg) == 0: continue
phone.send(msg.encode('
utf-8'
))
# print('
has send----->
'
)
data = phone.recv(1024)
# print('
has recv----->
'
)
print(data)
# 4、关闭
phone.close()
# >
>
: 啊
# b'
a'

# >
>
: 啊啊
# b'
\xb0\xa1\xb0\xa1'

# >
>
: 3、地址占用问题

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的time_wait状态的优化方法)

1、 方法一:加入一条socket配置,重用ip和端口#

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('
127.0.0.1'
,8080)) 2、 方法二:通过调整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 时间 4、模拟ssh远程执行命令

服务端通过subprocess执行该命令,然后返回命令的结果。

服务端:

from socket import *
import subprocess
server = socket(AF_INET, SOCK_STREAM)
server.bind(('
127.0.0.1'
, 8000))
server.listen(5)
print('
start...'
)
while True:
conn, client_addr = server.accept()
while True:
print('
from client:'
, client_addr)
cmd = conn.recv(1024)
if len(cmd) == 0: break
print('
cmd:'
, cmd)
obj = subprocess.Popen(cmd.decode('
utf8'
), # 输入的cmd命令
shell=True, # 通过shell运行
stderr=subprocess.PIPE, # 把错误输出放入管道,以便打印
stdout=subprocess.PIPE) # 把正确输出放入管道,以便打印

stdout = obj.stdout.read() # 打印正确输出
stderr = obj.stderr.read() # 打印错误输出

conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()

客户端

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('
127.0.0.1'
, 8000))
while True:
data = input('
please enter your data'
)
client.send(data.encode('
utf8'
))
data = client.recv(1024)
print('
from server:'
, data)
client.close()

输入dir命令,由于服务端发送字节少于1024字节,客户端可以接受。

输入tasklist命令,由于服务端发送字节多于1024字节,客户端只接受部分数据,并且当你再次输入dir命令的时候,客户端会接收dir命令的结果,但是会打印上一次的剩余未发送完的数据,这就是粘包问题。

5、粘包1、发送端需要等缓冲区满才发送出去,造成粘包

发送数据时间间隔很短,数据量很小,会合到一起,产生粘包。

服务端

# _*_coding:utf-8_*_
from socket import *
ip_port = ('
127.0.0.1'
, 8080)
TCP_socket_server = socket(AF_INET, SOCK_STREAM)
TCP_socket_server.bind(ip_port)
TCP_socket_server.listen(5)
conn, addr = TCP_socket_server.accept()

data1 = conn.recv(10)
data2 = conn.recv(10)
print('
----->
'
, data1.decode('
utf-8'
))
print('
----->
'
, data2.decode('
utf-8'
))
conn.close()

客户端

# _*_coding:utf-8_*_
import socket
BUFSIZE = 1024
ip_port = ('
127.0.0.1'
, 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
res = s.connect_ex(ip_port)
s.send('
hello'
.encode('
utf-8'
))
s.send('
world'
.encode('
utf-8'
))

# 服务端一起收到b'
helloworld'
2、接收方不及时接收缓冲区的包,造成多个包接收

客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包。

服务端

# _*_coding:utf-8_*_
from socket import *
ip_port = ('
127.0.0.1'
, 8080)
TCP_socket_server = socket(AF_INET, SOCK_STREAM)
TCP_socket_server.bind(ip_port)
TCP_socket_server.listen(5)
conn, addr = TCP_socket_server.accept()
data1 = conn.recv(2) # 一次没有收完整
data2 = conn.recv(10) # 下次收的时候,会先取旧的数据,然后取新的
print('
----->
'
, data1.decode('
utf-8'
))
print('
----->
'
, data2.decode('
utf-8'
))
conn.close()

客户端

# _*_coding:utf-8_*_
import socket
BUFSIZE = 1024
ip_port = ('
127.0.0.1'
, 8080)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
res = s.connect_ex(ip_port)
s.send('
hello feng'
.encode('
utf-8'
)) 6、解决粘包问题1、先发送的字节流总大小(low版)

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

为何low:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

服务端:

import socket, subprocess
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('
127.0.0.1'
, 8000))
server.listen(5)
while True:
conn, addr = server.accept()
print('
start...'
)
while True:
cmd = conn.recv(1024)
print('
cmd:'
, cmd)
obj = subprocess.Popen(cmd.decode('
utf8'
),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stdout = obj.stdout.read()
if stdout:
ret = stdout
else:
stderr = obj.stderr.read()
ret = stderr
ret_len = len(ret)
conn.send(str(ret_len).encode('
utf8'
))
data = conn.recv(1024).decode('
utf8'
)
if data == '
recv_ready'
:
conn.sendall(ret)
conn.close()
server.close()

客户端:

import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('
127.0.0.1'
, 8000))
while True:
msg = input('
please enter your cmd you want>
>
>
'
).strip()
if len(msg) == 0: continue
client.send(msg.encode('
utf8'
))
length = int(client.recv(1024))
client.send('
recv_ready'
.encode('
utf8'
))
send_size = 0
recv_size = 0
data = b'
'

while recv_size <
length:
data = client.recv(1024)
recv_size += len(data)
print(data.decode('
utf8'
)) 2、自定义固定长度报头(struct模块)

struct模块解析

import struct
import json
# '
i'
是格式
try:
obj = struct.pack('
i'
, 1222222222223)
except Exception as e:
print(e)
obj = struct.pack('
i'
, 1222)
print(obj, len(obj))
# '
i'
format requires -2147483648 <
= number <
= 2147483647
# b'
\xc6\x04\x00\x00'
4

res = struct.unpack('
i'
, obj)
print(res[0])
# 1222

解决粘包问题的核心就是:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

1、 使用struct模块创建报头:

import json
import struct
header_dic = {
'
filename'
: '
a.txt'
,
'
total_size'
:111111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222223131232,
'
hash'
: '
asdf123123x123213x'

}
header_json = json.dumps(header_dic)
header_bytes = header_json.encode('
utf-8'
)
print(len(header_bytes))# 223
# '
i'
是格式
obj = struct.pack('
i'
, len(header_bytes))
print(obj, len(obj))
# b'
\xdf\x00\x00\x00'
4

res = struct.unpack('
i'
, obj)
print(res[0])
# 223

2、服务端:

from socket import *
import subprocess
import struct
import json
server = socket(AF_INET, SOCK_STREAM)
server.bind(('
127.0.0.1'
, 8000))
server.listen(5)
print('
start...'
)
while True:
conn, client_addr = server.accept()
print(conn, client_addr)
while True:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode('
utf8'
),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
stderr = obj.stderr.read()
stdout = obj.stdout.read()
# 制作报头
header_dict = {
'
filename'
: '
a.txt'
,
'
total_size'
: len(stdout) + len(stderr),
'
hash'
: '
xasf123213123'

}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode('
utf8'
)
# 1. 先把报头的长度len(header_bytes)打包成4个bytes,然后发送
conn.send(struct.pack('
i'
, len(header_bytes)))
# 2. 发送报头
conn.send(header_bytes)
# 3. 发送真实的数据
conn.send(stdout)
conn.send(stderr)
conn.close()
server.close()

3、 客户端:

from socket import *
import json
import struct
client = socket(AF_INET, SOCK_STREAM)
client.connect(('
127.0.0.1'
, 8000))
while True:
cmd = input('
please enter your cmd you want>
>
>
'
)
if len(cmd) == 0: continue
client.send(cmd.encode('
utf8'
))
# 1. 先收4个字节,这4个字节中包含报头的长度
header_len = struct.unpack('
i'
, client.recv(4))[0]
# 2. 再接收报头
header_bytes = client.recv(header_len)
# 3. 从包头中解析出想要的东西
header_json = header_bytes.decode('
utf8'
)
header_dict = json.loads(header_json)
total_size = header_dict['
total_size'
]
# 4. 再收真实的数据
recv_size = 0
res = b'
'

while recv_size <
total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode('
utf8'
))
client.close() 二、基于UDP协议的socket套接字编程
  • UDP是无链接的,先启动哪一端都不会报错,并且可以同时多个客户端去跟服务端通信

  • UDP协议是数据报协议,发空的时候也会自带报头,因此客户端输入空,服务端也能收到。

  • UPD协议一般不用于传输大数据。

  • UPD套接字无粘包问题,但是不能替代TCP套接字,因为UPD协议有一个缺陷:如果数据发送的途中,数据丢失,则数据就丢失了,而TCP协议则不会有这种缺陷,因此一般UPD套接字用户无关紧要的数据发送,例如qq聊天。

UDP套接字简单示例1、服务端import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP
server.bind(('
127.0.0.1'
, 8080))
while True:
data, client_addr = server.recvfrom(1024)
print('
===>
'
, data, client_addr)
server.sendto(data.upper(), client_addr)
server.close() 2、客户端import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》UDP
while True:
msg = input('
>
>
: '
).strip() # msg='
'

client.sendto(msg.encode('
utf-8'
), ('
127.0.0.1'
, 8080))
data, server_addr = client.recvfrom(1024)
print(data)
client.close() 三、基于socketserver实现并发的socket编程1、基于TCP协议

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)。

1、 server类

2、 request类

基于tcp的socketserver我们自己定义的类中的。

  • self.server即套接字对象

  • self.request即一个链接

  • self.client_address即客户端地址

3、 服务端import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
# 通信循环
while True:
# print(self.client_address)
# print(self.request) #self.request=conn
try:
data = self.request.recv(1024)
if len(data) == 0: break
self.request.send(data.upper())
except ConnectionResetError:
break
if __name__ == '
__main__'
:
s = socketserver.ThreadingTCPServer(('
127.0.0.1'
, 8080), MyHandler, bind_and_activate=True)
s.serve_forever() # 代表连接循环
# 循环建立连接,每建立一个连接就会启动一个线程(服务员)+调用Myhanlder类产生一个对象,调用该对象下的handle方法,专门与刚刚建立好的连接做通信循环 4、 客户端import socket
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
phone.connect(('
127.0.0.1'
, 8080)) # 指定服务端ip和端口
while True:
# msg=input('
>
>
: '
).strip() #msg='
'

msg = '
client33333'
# msg='
'

if len(msg) == 0: continue
phone.send(msg.encode('
utf-8'
))
data = phone.recv(1024)
print(data)
phone.close() 2、基于UDP协议

基于udp的socketserver我们自己定义的类中的

  • self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'
    adsf'
    , )

  • self.client_address即客户端地址

1、 服务端import socketserver
class MyHandler(socketserver.BaseRequestHandler):
def handle(self):
# 通信循环
print(self.client_address)
print(self.request)
data = self.request[0]
print('
客户消息'
, data)
self.request[1].sendto(data.upper(), self.client_address)
if __name__ == '
__main__'
:
s = socketserver.ThreadingUDPServer(('
127.0.0.1'
, 8080), MyHandler)
s.serve_forever() 2、 客户端import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 数据报协议-》udp
while True:
# msg=input('
>
>
: '
).strip() #msg='
'

msg = '
client1111'

client.sendto(msg.encode('
utf-8'
), ('
127.0.0.1'
, 8080))
data, server_addr = client.recvfrom(1024)
print(data)
client.close() 四、Python Internet 模块

以下列出了 Python 网络编程的一些重要模块:

协议功能用处端口号Python 模块HTTP网页访问80httplib, urllib, xmlrpclibNNTP阅读和张贴新闻文章,俗称为"
帖子"
119nntplibFTP文件传输20ftplib, urllibSMTP发送邮件25smtplibPOP3接收邮件110poplibIMAP4获取邮件143imaplibTelnet命令行23telnetlibGopher信息查找70gopherlib, urllib

Socket是计算机网络编程中一种重要的通信方式,Python标准库中的socket模块为我们提供了基础的网络编程功能。而socketserver模块则是基于socket的服务器框架,为我们构建高效的网络服务提供了很大的帮助。
1. socket模块:基础网络编程
Python的socket模块提供了一种网络通信的编程方式,它实现了常见的socket接口,方便我们实现基于TCP/UDP协议的网络通信。通过socket模块,我们可以方便地创建、绑定、监听、发送和接收网络数据。
2. socketserver模块:高效网络服务
Python标准库中的socketserver模块则是基于socket实现的服务器框架。使用socketserver模块,我们只需要继承并实现一个简单的接口,就可以轻松地构建高效的网络服务。
3. 基于TCP的socket编程
TCP协议是一种面向连接的协议,使用TCP协议进行通信时,通信双方需要建立连接,并在连接存在的情况下进行数据的传输。在Python中使用socket编程实现TCP通信,需要创建socket对象、绑定IP地址和端口号、监听客户端连接、接收和发送客户端数据等步骤。
4. 基于UDP的socket编程
UDP协议是一种无连接的协议,它使用不可靠的发送和接收机制进行数据的传输。在Python中使用socket编程实现UDP通信,同样需要创建socket对象、绑定IP地址和端口号、发送和接收数据等步骤。
总之,Python的socket和socketserver模块为我们实现网络编程提供了非常方便的方法。通过学习和使用这两个模块,我们可以轻松地构建出高效、稳定的网络服务,实现更加复杂的网络通信需求。