作者:忠讧_136 | 来源:互联网 | 2023-06-09 12:41
socketsocket通常被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
socket
socket 通常被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过socket这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
Python标准库提供了socket模块来实现这种网络通信。实例化一个socket类便能得到一个socket对象sock = socket.socket()
,使用这个socket对象就可以进行通信了。常用的socket有两种。
SOCK_STREAM |
面向连接的流式socket,基于TCP协议 |
SOCK_DGRAM |
无连接的数据报式socket,基于UDP协议 |
相同类型的socket才能正常的通信,因为他们都有各自发送和接收消息的协议。
socket对象
import socket
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 8000
TCP 127.0.0.1:8000 0.0.0.0:0 LISTENING
下面构建一个完整的TCP服务器。这是基本的服务器和客户端通信结构图。根据结构图构建聊天服务器
简单步骤和思路
- 客户端连接到来后,开启新线程与该客户端交互,发送和接收消息。(recv和send)
通过以上分析,我们需要使用多线程,分别与服务器交互,等待客户端连接,与一个连接后的客户端交互;每当成功的连接一个客户端,都需要新启动一个线程进行交互。
import socket
import threading
class Server:
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): # 该线程等待连接并创建处理线程
while True:
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): # 每个客户端开启一个线程与其交互
while True:
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())
def stop(self):
with self.lock:
for s in self.socks.values():
s.close()
s = Server()
s.start()
while True:
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()方法,使用文件对象进行收发数据。
import socket
import threading
import datetime
class Client:
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() # 主进程发送消息
def send(self):
while True:
msg = input(">>>").strip()
self.f.write(msg)
self.f.flush()
if msg == "quit":
self.stop()
break
def recv(self):
while True:
msg = self.f.readline()
print("server:{}{:%Y/%m/%d %H:%M:%S}\n\t{}".format(self._sock.getpeername(), datetime.datetime.now(), msg))
def stop(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方法对应一个读取缓冲区,每次从读取缓冲区中读取数据,缓冲区没有数据可读取将会发生阻塞等待。