socket
socket 通常被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
Python标准库提供了socket模块来实现这种网络通信。实例化一个socket类便能得到一个socket对象sock = socket.socket(),使用这个socket对象就可以进行通信了。常用的socket有两种。
SOCK_STREAM
面向连接的流式socket,基于TCP协议
SOCK_DGRAM
无连接的数据报式socket,基于UDP协议
相同类型的socket才能正常的通信,因为他们都有各自发送和接收消息的协议。
socket对象
importsocket
s= socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None)
实例化时指定对应的参数可以得到不同类型的socket,默认使用IPV4和TCP协议的类型
参数
可选值
说明
family
socket.AF_UNIX
只能够用于单一的Unix系统进程间通信
socket.AF_INET
默认使用IPv4协议
socket.AF_INET6
使用IPv6协议
type
socket.SOCK_STREAM
面向连接的流式socket,基于TCP协议
socket.SOCK_DGRAM
无连接的数据报式socket,基于UDP协议
实践
通过写一个聊天的服务器和客户端体验这种通信
TCP服务端
使用socket构建一个最简单TCP服务器可接收客户端的连接。我们需要一个socket用于网络通信,并监听一个地址和端口,等待其他的网络连接访问该端口,代码如下。
server = socket.socket() #创建
server.bind((‘127.0.0.1’, 8000)) #绑定本机地址和端口
server.listen()#开始监听端口
#阻塞等待客户端的连接,连接后返回一个新的可与客户端通信的socket和客户端的(ip,port)
s, raddr = server.accept()
当执行上面的python程序后,操作系统将会启动一个进程,该服务进程正在监听8000端口,在Windows命令行中使用netstat -anp tcp | findstr 8000查询监听状态。在Linux上可以使用ss -tanl | grep 8000命令查看。
C:\Users\user>netstat -anp tcp | findstr 8000TCP127.0.0.1:8000 0.0.0.0:0 LISTENING
下面构建一个完整的TCP服务器。这是基本的服务器和客户端通信结构图。根据结构图构建聊天服务器
简单步骤和思路
创建socket
绑定一个ip地址和端口
开始监听(listen)
阻塞等待连接(accept)
客户端连接到来后,开启新线程与该客户端交互,发送和接收消息。(recv和send)
同时我们使用主线程操作服务端退出。
通过以上分析,我们需要使用多线程,分别与服务器交互,等待客户端连接,与一个连接后的客户端交互;每当成功的连接一个客户端,都需要新启动一个线程进行交互。
importsocketimportthreadingclassServer:def __init__(self, ip=’127.0.0.1′, port=8000): #设置默认值
self.addr =ip, port
self.lock=threading.Lock()
self.sock=socket.socket()
self.sock.bind(self.addr)
self.socks= {“accept”: self.sock} #将所有创建的socket都放字典,方便释放
def start(self): #启动接口
self.sock.listen()
threading.Thread(target=self.accept, name=”accept”, daemon=True).start()def accept(self): #该线程等待连接并创建处理线程
whileTrue:
s, raddr=self.sock.accept()
with self.lock:
self.socks[raddr]=s
threading.Thread(target=self.recv, args=(s, raddr), name=”recv”, daemon=True).start()def recv(self, s, raddr): #每个客户端开启一个线程与其交互
whileTrue:
data= s.recv(1024).decode()if data.strip() == “” or data.strip() == “quit”: #客户端结束条件
with self.lock:
self.socks.pop(raddr)
s.close()break
print(data)
s.send(“server:{}\n”.format(data).encode())defstop(self):
with self.lock:for s inself.socks.values():
s.close()
s=Server()
s.start()whileTrue:
cmd= input(“server commond:>>>”)if cmd == “quit”: #服务器退出条件
s.stop()break
print(threading.enumerate())
我们需要注意的问题:
服务端需要与多个不同客户端进行交互,所以我们需要开启不同线程去处理各自的业务,为了服务端在启动后可以获得控制权,我们使用主线程来与服务器管理者交互,使用命令行输入指令就能在服务器启动后与服务器做一些交互,例如代码中的强制关闭服务器,并在强制关闭服务前提前关闭掉这些socket对象。在遍历字典来关闭socket对象时,我们使用了锁,要求在这个遍历操作完成前,其他线程无法进行增加或者删除操作,保证了字典遍历时的线程安全。
socket常用的方法
方法
含义
服务端
s.bind(address)
将套接字绑定到地址,以元组(host,port)的形式表示地址
s.listen(backlog)
开始监听TCP传入连接。backlog:操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了
s.accept()
接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址,为一个元组
客户端socket函数
s.connect(address)
连接到address处的套接字,格式为元组(hostname,port),如果连接出错,返回socket.error错误
s.connect_ex(adddress)
功能与connect(address)相同,但是成功返回0,失败返回errno的值
公共socket函数
s.recv(bufsize[,flag])
从s接受bytes类型的数据,有数据就接受返回,bufsize指定要接收的最大数据量
s.send(bytes[,flag])
TCP发送数据。将bytes中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于bytes的字节大小
s.sendall(bytes[,flag])
发送全部TCP数据。将bytes中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常
sendfile()
使用os.sendfile()高效的发送文件的方法,必须使用SOCK_STREAM类型的套接字才能使用
s.recvfrom(bufsize[.flag])
接受UDP套接字的数据。与recv()类似,但返回值是(data,address)。其中data是包含接收数据的bytes,address是发送方地址
s.sendto(string[,flag],address)
发送UDP数据。address是形式为(ipaddr,port)的元组。返回值是发送的字节数
s.getpeername()
返回连接套接字的远程地址(ipaddr,port)
s.getsockname()
返回套接字自己的地址(ipaddr,port)
s.setsockopt(level,optname,value)
设置给定套接字选项的值
s.getsockopt(level,optname[.buflen])
返回套接字选项的值
s.settimeout(timeout)
设置套接字操作的超时间,值为None表示没有超时期。一般超时期在创建时设置
s.gettimeout()
返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None
s.fileno()
返回套接字的文件描述符
s.setblocking(flag)
设置阻塞模式,非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
s.makefile()
创建一个与该套接字相关连的文件,返回一个类文件对象,可是使用文件操作发送和接收数据
sendfile是一个高效的传送方式,文件数据始终处于内核态,在操作系统缓冲区直接发送,不会到应用层缓冲区。
使用makefile方法将返回该socket对应的文件对象(io.TextIOWrapper),该对象的write()等价于send()方法, read方法等价于recv(),还可以使用readline等方法。这样我们可以使用文件的接口去收发信息,客户端将使用这种方式与服务器交互。
sock =socket.socket()
file= sock.makefile(“rw”) #mode=”rw” 可读可写
data= file.read() #等价于socket.recv()
data= file.read(10) #指定读取字符大小长度,满10个字符才会返回。
data = file.readlin() #每次读取一行,遇到换行符才返回。#写入数据
msg = “hello world”file.write(msg)
file.flush()#手动flush,否则在缓冲区满或者退出时自动才写入socket。同文件写入操作
TCP客户端
相比于服务端,客户端只需要连接服务器后发送和接受消息即可,相对更容易实现。
客户端需要同时接受和发送消息,而这两个操作均会阻塞,所以两个功能需要在不同的线程。下面代码使用了socket的makefile()方法,使用文件对象进行收发数据。
importsocketimportthreadingimportdatetimeclassClient:def __init__(self, rip, rport): #服务器ip 和 端口
self._raddr =rip, rport
self._sock=socket.socket()
self._connect()def_connect(self):
self._sock.connect(self._raddr)#尝试连接指定的地址
self.f = self._sock.makefile(“rw”)
self.f.write(“i am client at {}\n”.format(self._sock.getsockname()))
self.f.flush()
threading.Thread(target=self.recv, name=”recv”, daemon=True).start() #一个进程接收消息
self.send() #主进程发送消息
defsend(self):whileTrue:
msg= input(“>>>”).strip()
self.f.write(msg)
self.f.flush()if msg == “quit”:
self.stop()break
defrecv(self):whileTrue:
msg=self.f.readline()print(“server:{}{:%Y/%m/%d %H:%M:%S}\n\t{}”.format(self._sock.getpeername(), datetime.datetime.now(), msg))defstop(self):
self.f.close()
self._sock.close()
c= Client(“127.0.0.1”, 8000)
客户端使用connect()方法将会尝试连接服务器(这个服务必须存在,否则无法连接),由于服务基于TCP协议,所以在connect()连接时候,实际上会进行TCP三次握手的连接,但是我们在应用层面无法感知到这个下层行为。同样的在进行close关闭socket时,在断开连接前将会进行四次挥手操作。
使用makefile后会得到该socket的文件对象,在进行read和write时会先将数据放入缓冲区暂存,write方法对应一个发送缓冲区,将需要发送到对方的数据暂存到该缓冲区,在调用flush时才会将数据发送,当写入缓冲区满了而没有及时发送数据,发送数据没有缓存空间可用,将会发生阻塞等待。同样read方法对应一个读取缓冲区,每次从读取缓冲区中读取数据,缓冲区没有数据可读取将会发生阻塞等待。