您的位置:首页 > 编程语言 > Python开发

python多种方式实现多客户端多线程网络通信

2020-06-01 04:36 148 查看

最近一直在研究python网络编程的知识,然后也把最近的一些研究的知识整理成博文,分享给大家,希望大家喜欢。

整体核心内容包括:

1、socket、TCP、UDP基础知识

2、TCP和UDP的单线程实现模式

3、TCP的多线程实现模式

4、TCP的多线程server实现模式

5、TCP和SELECT库实现多线程模式

一、socket、TCP、UDP基础知识

1、socket介绍

网络编程中使用的传输协议主要有 TCP 和 UDP,其中:

  • TCP 需要建立连接,是可靠的、基于字节流的协议,通常与 IP 协议共同使用;
  • UDP 不需要建立连接,可靠性差,但速度更快。

socket(套接字),应用程序可以通过它发送或接收数据,套接字允许应用程序将 I/O 插入到网络中,并与网络中的其他应用程序进行通信。

Python 中通过 socket() 函数来创建套接字对象,具体格式如下:

[code]socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

family:套接字协议族,可以使用 AF_UNIX(只能用于单一的 Unix 系统进程间通信)、AF_INET(服务器之间网络通信)

type:套接字类型,可以使用 SOCK_STREAM(面向连接的)、SOCK_DGRAM(非连接的)

2、TCP介绍

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内 [1] 另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

为了让tcp通信更加方便需要引入一个socket模块(将网络层、数据链路层、物理层封装的模块),我们只要调用模块中的相关接口就能实现传输层下面的繁琐操作。

3、UDP介绍

UDP叫数据报协议,意味着发消息都带有数据报头,udp的server不需要就行监听也不需要建立连接 在启动服务之后只能被动的等待客户端发送消息过来,客户端发送消息的时候,要带上服务端的地址 服务端在回复消息的时候,也需要带上客户端的地址 有如下几个特点:1.udp协议客户端允许发空 2.udp协议不会粘包 3.udp协议服务端不存在的情况下,客户端照样不会报错 4.udp协议支持并发。

二、TCP和UDP的单线程实现模式

下面的代码比较简洁,但是由于send和recv都是阻塞运行的函数,因此一次只能支持一个客户端接入,而且也只能单线程接入。譬如只能先收后发、或者先发后收,无法按需只接收数据或者按需只发送数据。

1、TCP的实现方式:

服务器端:

[code]from socket import *
# 确定服务端传输协议
server = socket(AF_INET, SOCK_STREAM)
# 这里的SOCK_STREAM代表的就是流式协议TCP,如果是SOCK_DGRAM就代表UDP协议
# 防止启动的时候地址端口被占用,未被释放,可在bind前添加
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
# 固定服务端IP和PORT,让客户端能够通过IP和端口访问服务端
server.bind(('127.0.0.1', 8080))
# ('127.0.0.1', 8080)这里必须用元组形式传入IP和PORT,本地访问本地IP默认为'127.0.0.1'
# 设置半连接池数量(一般为5)
# 最大等待数(有很多人理解为最大连接数,其实是错误的)
server.listen(5)
# 半连接池:客户端连接请求个数的容器,当前已连接的客户端信息收发未完成前,会有最大5个客户端连接请求进入排队状态,
# 等待上一个通信完毕后,就可以连接进入开始通信。
# 双向通道建立成功,可以进行下一步数据的通信了
while True: # 循环监听
print('等待客户端发送信息...')
conn, client_addr = server.accept()
# 进行一次信息的收与发
data = conn.recv(1024).decode('utf-8')    # 每次最大接收1024字节,收到的数据为二进制Bytes类型
#serObj.recv(1024).decode('utf-8')
print('服务端接收请求数据:' + data)
conn.send(data.upper().encode('utf-8'))   # 将收到的数据进行处理,返回新的数据,反馈给客户端(给客户端发数据),发的数据类型也必须是Bytes类型
# conn.sendall(data.upper().encode('utf-8'))
# 一轮信息收发完毕,关闭已经建立的双向通道
conn.close()

客户端:

[code]from socket import *
# 确定客户端传输协议(服务端和客户端服务协议一样才能进行有效的通信)
client = socket(AF_INET, SOCK_STREAM)  # 这里的SOCK_STREAM代表的就是流式协议TCP,如果是SOCK_DGRAM就代表UDP协议
# 开始连接服务端IP和PORT,建立双向链接
client.connect(('127.0.0.1', 8080))  # 通过服务端IP和PORT进行连接
#返回连接套接字的远程地址,类型通常是元组 (ipaddr,port)
print(client.getpeername())
# 返回套接字自己的地址,通常是一个元组 (ipaddr,port)
print(client.getsockname())
# 走到这一步就已经建立连接完毕,接下来开始数据通信:
client.send('hello,python'.encode('utf-8'))    # 将发送的信息转码成Bytes类型数据
data = client.recv(1024)  # 每次最大收数据大小为1024字节(1kb)
print('客户端接收响应数据:' + data.decode('utf-8')) # 将b类型数据转换成字符串格式
# 一次传输完毕
client.close()    # 关闭客户端连接

2、UDP的实现方式:

服务器端:

[code]import socket
# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 绑定地址
s.bind(('127.0.0.1', 8888))
while True:
# 接收数据
data, addr = s.recvfrom(1024)
print('服务端接收请求数据:' + data.decode('utf-8'))
# 响应数据
s.sendto(data.decode('utf-8').upper().encode('utf-8'), addr)

客户端:

[code]import socket
# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 向服务端发送数据
s.sendto(b'hello', ('127.0.0.1', 8888))
# 接受服务端响应数据
data = s.recv(1024).decode('utf-8')
print('客户端接收响应数据:' + data)
# 关闭
s.close()

3、TCP的单客户端多线程实现模式

上面实现的只是单客户端单线程的处理模式,并不是真正的网络编程,只是相当于入门的级别,即通过上述的代码能够简单了解TCP和UDP的工作机制、相关的函数调用方式和整体实现流程,那么接下来主要以TCP实现单客户端的多线程模式(即支持单客户端可以按需只收数据、或者按需只发数据、或者同时收发数据)。

在这个案例中,使用主线程进行接收数据、使用子线程进行发送数据。

服务器端:

[code]import socket
import sys
import threading
import time

def server():
def bind():
HOST = '127.0.0.1'
port = 8888
s.bind((HOST, port))
def listen():
s.listen(10) # 最大等待数
print('Socket now listening')
def send(conn):
while True:
try:
sth = input('say something:\n')
conn.sendall(sth.encode('utf-8'))
except ConnectionError:
print('connect error')
sys.exit(-1)
except:
print('unexpect error')
sys.exit(-1)
def recv(conn):
while True:
try:
data = conn.recv(1024)
data2 = data.decode('utf-8')
print('get message:' + data2)
except ConnectionError:
print('connect error')
sys.exit(-1)
except:
print('unexpect error')
sys.exit(-1)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
bind()
listen()
conn, addr = s.accept()
print("connect success")
print('connect time: ' + time.ctime())
threading._start_new_thread(recv, (conn,))
send(conn)

if __name__ == '__main__':
server()

客户端:

[code]import socket
import sys
import threading
import time

class client():
def __init__(self):
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.ip = "127.0.0.1"
self.port = 8888
def connect(self):
try:
self.s.connect((self.ip, self.port))
print("connect success")
print('connect time: ' + time.ctime())
except ConnectionError:
print('connect error')
sys.exit(-1)
except:
print('unexpect error')
sys.exit(-1)

def send(self):
while True:
sth = input('say something:\n')
try:
self.s.sendall(sth.encode('utf-8'))
except ConnectionError:
print('connect error')
sys.exit(-1)
except:
print('unexpect error')
sys.exit(-1)

def receive(self):
while True:
try:
r = self.s.recv(1024)
print('get message:' + r.decode('utf-8'))
except ConnectionError:
print('connect error')
sys.exit(-1)
except:
print('unexpect error')
sys.exit(-1)

if __name__ == '__main__':
c1 = client()
c1.connect()
threading._start_new_thread(c1.receive, ())
c1.send()

4、TCP的多客户端实现模式

上述介绍了实现单客户端单线程方式和单客户端多线程方式,但是实际上服务器端还是一次只能一个客户端进行链接,并没有实现真正意义上的多客户端链接的方式,那么下面将进行延伸,实现多客户端的实现模式。

服务器端:

[code]import threading
import socket
import time
def func(cnn):
while True:
try:
request = cnn.recv(1024)
if len(request)==0:
cnn.close()
break
print('%s接收到来自客户端%s的信息,信息内容为:%s' % (time.strftime("%D %H:%M:%S"),cnn.getpeername(),request.decode('utf-8')))
cnn.send(b'%s' % request)
except ConnectionResetError:
pass

if __name__ == "__main__":
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))
s.listen(5)

while True:
print("等待客户端接入中")
cnn, addr = s.accept()
th = threading.Thread(target=func, args=(cnn,), daemon=True)
th.start()

客户端:

[code]from socket import *
import time
# 确定客户端传输协议(服务端和客户端服务协议一样才能进行有效的通信)
client = socket(AF_INET, SOCK_STREAM)  # 这里的SOCK_STREAM代表的就是流式协议TCP,如果是SOCK_DGRAM就代表UDP协议
# 开始连接服务端IP和PORT,建立双向链接
client.connect(('127.0.0.1', 8888))  # 通过服务端IP和PORT进行连接
#返回连接套接字的远程地址,类型通常是元组 (ipaddr,port)
print(client.getpeername())
# 返回套接字自己的地址,通常是一个元组 (ipaddr,port)
print(client.getsockname())
# 走到这一步就已经建立连接完毕,接下来开始数据通信:
while True:
client.send('hello,python'.encode('utf-8'))    # 将发送的信息转码成Bytes类型数据
data = client.recv(1024)  # 每次最大收数据大小为1024字节(1kb)
print('客户端接收响应数据:' + data.decode('utf-8')) # 将b类型数据转换成字符串格式
time.sleep(1)
# 一次传输完毕
client.close()    # 关闭客户端连接

运行效果:

5、TCP的多线程server实现模式

socketserver有四个基本的类,后两个不常用,这4个类处理并发请求都是同步的,他们其实不是多线程的,他们只是把socke封装了一下,加了一些方法,这里还没有实现多并发和多线程,这些方法不适用每个请求都耗费长时间才能完成 

# socketserver.TCPServer

# socketserver.UDPServer

# socketserver.UnixStreamServer

# socketserver.UnixDatagramServer

# socketserver.ForkingMixIn

# socketserver.ThreadingMixIn

1、TCP实现案例

服务器端:

[code]
import socketserver

class MyTcpServerClass(socketserver.BaseRequestHandler):
# 先定义一个自己的socket的类,所有和客户端交互的动作全部要在这里写,默认这里是空白的,且必须写handle这个防范
def handle(self):
print("等待新的连接:")
print("新的客户端:", self.client_address)
while True:
try:
data = self.request.recv(1024)
print("客户端:", self.client_address, str(data, encoding="utf-8"))
self.request.send(data)
except Exception:
print("客户端[%s]连接已经断开,服务端也将断开" % (self.client_address[1]))
self.request.close()
break

if __name__ == '__main__':
ip_bind = ("127.0.0.1", 9000)
server = socketserver.ThreadingTCPServer(ip_bind, MyTcpServerClass)
# 每次来一个连接,就构建一个实例
server.serve_forever()
# 让这个server一直运行

客户端:

[code]from socket import *
import time
# 确定客户端传输协议(服务端和客户端服务协议一样才能进行有效的通信)
client = socket(AF_INET, SOCK_STREAM)  # 这里的SOCK_STREAM代表的就是流式协议TCP,如果是SOCK_DGRAM就代表UDP协议
# 开始连接服务端IP和PORT,建立双向链接
client.connect(('127.0.0.1', 9000))  # 通过服务端IP和PORT进行连接
#返回连接套接字的远程地址,类型通常是元组 (ipaddr,port)
print(client.getpeername())
# 返回套接字自己的地址,通常是一个元组 (ipaddr,port)
print(client.getsockname())
# 走到这一步就已经建立连接完毕,接下来开始数据通信:
while True:
client.send('hello,python'.encode('utf-8'))    # 将发送的信息转码成Bytes类型数据
data = client.recv(1024)  # 每次最大收数据大小为1024字节(1kb)
print('客户端接收响应数据:' + data.decode('utf-8')) # 将b类型数据转换成字符串格式
time.sleep(1)
# 一次传输完毕
client.close()    # 关闭客户端连接

2、UDP实现案例:

服务器端:

[code]import socketserver

class MyUdpHandler(socketserver.BaseRequestHandler):
def handle(self):
data, sock = self.request
sock.sendto(data.upper(), self.client_address)

if __name__ == '__main__':
server = socketserver.ThreadingUDPServer(('127.0.0.1', 8081), MyUdpHandler)
server.serve_forever()

客户端:

[code]from _socket import *

client = socket(AF_INET, SOCK_DGRAM)

while True:
client.sendto(b'ddddddd', ('127.0.0.1', 8081))
data, addr = client.recvfrom(1024)
print(data.decode('utf-8'))

6、SELECT实现多线程模式

sokect编程算是python的知识了,这里面单独拿出来说主要是想说说select。一开始我是这样设计的在服务器端给一个连接进来的客户端一个线程去接收客户端发送的消息,但是这样会造成程序在退出的时候很多线程无法退出的问题。然后想了各种各样的办法,但是最后在网上看到一句话顿时让我醒悟————多线程会增加系统的不稳定性。所以我就果断放弃了socket的多线程编程转而使用了select技术。

1、select介绍

Python 中只需这么写:

[code]can_read, can_write, error_info = select.select(inputs, outputs, None, None)

其中:

第一个参数是我们需要监听可读的套接字,

第二个参数是我们需要监听可写的套接字,

第三个参数使我们需要监听异常的套接字,

第四个则是时间限制设置. 如果监听的套接字满足了可读可写条件, 那么所返回的can,read 或是 can_write就会有值了, 然后我们就可以利用这些返回值进行随后的操作了。

相比较unix 的select模型, 其select函数的返回值是一个整型, 用以判断是否执行成功. 第一个参数就是服务器端的socket, 第二个是我们在运行过程中存储的客户端的socket, 第三个存储错误信息。 重点是在返回值, 第一个返回的是可读的list, 第二个存储的是可写的list, 第三个存储的是错误信息的。

2、演示案例:

服务器端:

[code]import socket
import select
import time
sk1 = socket.socket()
sk1.bind(('0.0.0.0', 8001))
sk1.listen()
sk2 = socket.socket()
sk2.bind(('0.0.0.0', 8002))
sk2.listen()
sk3 = socket.socket()
sk3.bind(('0.0.0.0', 8003))
sk3.listen()
inputs = [sk1, sk2, sk3, ]
while True:
r_list, w_list, e_list = select.select(inputs,[],inputs,1)
for sk in r_list:
# conn表示每一个连接对象
conn, address = sk.accept()
conn.sendall(bytes('hello', encoding='utf-8'))
time.sleep(1)
print('r_list %s' % r_list)
# conn.close()
for sk in e_list:
inputs.remove(sk)
print('e_list %s' % e_list)

客户端:

[code]import socket
import time
obj = socket.socket()
obj.connect(('127.0.0.1', 8001))
content = str(obj.recv(1024), encoding='utf-8')
print(content)
time.sleep(10)
obj.close()

好的,讲到这里,关于python网络编程的基础知识已经基本讲完了,希望我花了大力气整理出来的知识点,对大家有帮助!

作为入门,主要是要关注网络接口的编写规范,里面单客户端、多客户端、多线程的实现模式,掌握好socket和socketserver的基本用法,并初步对select有一个大概的了解即可。

后续我将会使用上述的方法实现一个完整的类似QQ的聊天工具,这个工具将包括实现网络聊天、传文件、传图片、视频聊天等多种功能,欢迎各位朋友持续关注。也欢迎关注我的公众号(俊哥随笔)。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐