【Python】socket同时收发与多线程防止input阻塞
采薇采薇,薇亦作止。曰归曰归,岁亦莫止。靡室靡家,玁狁之故。不遑启居,玁狁之故。
采薇采薇,薇亦柔止。曰归曰归,心亦忧止。忧心烈烈,载饥载渴。我戍未定,靡使归聘。
采薇采薇,薇亦刚止。曰归曰归,岁亦阳止。王事靡盬,不遑启处。忧心孔疚,我行不来。
彼尔维何?维常之华。彼路斯何?君子之车。戎车既驾,四牡业业。岂敢定居?一月三捷。
驾彼四牡,四牡骙骙。君子所依,小人所腓。四牡翼翼,象弭鱼服。岂不日戒,玁狁孔棘。
昔我往矣,杨柳依依。今我来思,雨雪霏霏。行道迟迟,载渴载饥。我心伤悲,莫知我哀!
——《诗经 · 小雅 · 采薇》
一、What
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
——来自百度百科
二、Why
学Python也有一段时间了,socket也经常用,不过都是单纯发送数据或者接收数据而已,因为业务上没有要求收发同时存在(我接触的设备都是只接收指令,不返回数据),最近正好有这方面的想法,觉得也不难实现,就把接收的逻辑以及同时接收发送的逻辑一起完善一下。
要实现收发,首要的问题是解决发送和接收一起堵塞的问题,我们可以先写一个简单的发送程序和接收程序,然后再想办法合并起来。
三、How
3.1、Client
3.1.1、单纯的发送一段数据
1、代码
# -*- coding: cp936 -*-
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('10.3.1.67',8001))
s.send("单纯的发送一段数据".encode('gb18030'))
s.close()
2、说明
【# -- coding: cp936 --】代表使用中文编码的gb2312,这在发送和接收数据需要指定编码格式时会用到,还有另外一种写法:#coding=cp936,效果是一样的,但是这种的区别在于,前者支持更多的编辑器,比如Emacs等编辑器使用# -- coding: cp936 --这种方式进行编码声明。
import socket表示引入socket这个库。
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
初始化socket,构造函数中包含了两个参数,分别是,AF_INET是设置地址格式为IPV4,SOCK_STREAM参数表示使用TCP的传输方式
s.connect(('10.3.1.67',8001))
连接到socket的服务端,此时我们的程序作为客户端,通过IP和端口号去连接服务端
s.send("单纯的发送一段数据".encode('gb18030'))
"单纯的发送一段数据”作为字符串,要经过编码处理,转换为bytes格式,才可以发送
s.close()
关闭TCP 链接,结束通讯
详细内容可参考
https://blog.csdn.net/zzyandzzzy/article/details/72236388
的介绍。
3、实现效果

3.1.2、自己输入要发送的数据
如果我想要自己输入数据发送呢?使用input方法即可,它会从控制台接收数据并传入程序。
1、代码
# -*- coding: cp936 -*-
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('10.3.1.67',8001))
a=input("请输入一段数据并按回车:")
s.send(a.encode('gb18030'))
s.close()
2、实现效果

3、然后我们加一个循环,可以不断的发送数据
# -*- coding: cp936 -*-
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('192.168.0.106',8001))
while 1:
s.send(input('请输入数据并回车:').encode('gb18030'))
# s.close()
3.1.3、实现只接收数据的逻辑
1、代码:
# -*- coding: cp936 -*-
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('10.3.1.67', 8001))
while 1:
a = s.recv(100)
print(a.decode('gb18030'))
# s.close()
2、说明
使用while 1使程序进去死循环,处于不断的接收数据的状态,然后a = s.recv(100)指定单次接收的最大数据量为100个字节,并将接收到的数据存入a中,最后通过decode函数将bytes格式转化为字符串并打印,最后因为死循环,所以关闭函数不起作用,使用#注释掉

3.1.4、把两段代码合在一起
最简单的形式就是直接把发送数据的代码复制过来
1、代码如下:
# -*- coding: cp936 -*-
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('10.3.1.67', 8001))
while 1:
a = input("请输入一段数据并按回车:")
s.send(a.encode('gb18030'))
a = s.recv(100)
print(a.decode('gb18030'))
# s.close()
2、实现效果

这可以算是初步实现效果,我查询了一些网上的socket收发也是这么写的,不过多试几次你会发现,当我们使用工具发送数据的时候,Python程序的控制台是不显示的,此时程序停留在
a = input(“请输入一段数据并按回车:”)
这一行,等待输入,只有你在控制台这里输入内容并回车后才可以显示TCP工具发送的数据,因为input这个方法是线程阻塞的,如果想实现实时收发,需要使用多线程,一个线程接收,一个线程发送。
3.1.5、同时收和发送
1、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.connect(('172.19.6.236', 8080))
class A(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
while True:
c = input('小明:')
s.send(bytes(c, encoding='gb18030'))
a = A()
a.start()
while True:
b = s.recv(1024)
print('\n小红:', str(b, encoding='gb18030'))
2、实现效果

3、说明
我们定义了一个类A,并且继承了多线程的threading.Thread类,然后在构造函数中初始化这个类的实例self,之后在run方法(实际上是Thread类的方法,在这里被重写)中实现发送数据的逻辑。接着我们新建了对象a,并且启动该线程。然后再写一个循环,用来接收数据。
3.2 Server
3.2.1 简单的发送一段数据
考虑一下思路,如果我们要使客户端可以连接我们,那么需要先绑定一个端口并且监听,我们可以用最简单的127.0.0.1,就是自己的端口,然后绑定上去,等客户端来连接。
1、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(2)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
c.send('简单的发送一段数据'.encode('gb2312'))
c.close()
s.close()
2、实现效果

3、说明
注意这里的执行顺序,我首先运行我的程序,此时程序会阻塞在accept这里,然后我在TCP工具那里选择TCP客户端,输入127的地址和端口号,点击连接,接收区立刻收到一串数据,然后我程序也结束了。
注意accept这个方法返回两个值,第一个是专门用来通讯的套接字,之前那个绑定了IP的套接字s是专门用来监听和连接的,这样的好处是多个连接不会导致数据混乱,保证点对点数据传输。
3.2.2 自定义发送的数据
1、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(2)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
c.send(input('请输入:').encode('gb2312'))
c.close()
s.close()
2、效果

3.2.3 循环输入
1、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(2)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
while True:
c.send(input('请输入:').encode('gb2312'))
# c.close()
# s.close()
2、效果

3、说明
这种方式很容易阻塞,一旦停止掉服务端的程序,那么再启用就会提示

可以通过在终端kill掉这个线程来实现重新连接。

注意第一句前面可能要加sudo,否则无法查询。
3.2.4 接收
1、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(2)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
while True:
print(c.recv(1024).decode('gb2312'))
# c.close()
# s.close()
2、效果

3.2.5 结合
1、效果

2、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(5)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
while True:
c.send(input('请输入:').encode('gb2312'))
print(c.recv(1024).decode('gb2312'))
# c.close()
# s.close()
3.2.6 多线程
1、效果

2、代码
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8001))
# 设置最大连接数,超过后排队
s.listen(5)
# 进入循环,不断的接受客户端的请求
# accept返回两个值,
# 第一个是新的套接字c,用于通讯,s这个套接字专门用来连接
# 第二个是字符串a,对方的IP地址
c, a = s.accept()
class A(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
while True:
b = c.recv(1024)
if len(b) > 0:
print('\n小红:', str(b, encoding='gb18030'))
e = A()
e.start()
while True:
d = input('小明:')
c.send(bytes(d, encoding='gb18030'))
# c.close()
# s.close()
3.2.7 用自己的Client程序
1、效果


2、客户端代码:
# -*- coding:cp936 -*-
import socket, threading
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.connect(('127.0.0.1', 8001))
class A(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
while True:
b = s.recv(1024)
if len(b) > 0:
print('\n小明:', str(b, encoding='gb18030'))
a = A()
a.start()
while True:
c = input('小红:')
s.send(bytes(c, encoding='gb18030'))
完。