python网络通信库_python网络通信 — socket

  • Post author:
  • Post category:python


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服务器。这是基本的服务器和客户端通信结构图。根据结构图构建聊天服务器

1658264-20200609111036301-627283301.png

简单步骤和思路

创建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方法对应一个读取缓冲区,每次从读取缓冲区中读取数据,缓冲区没有数据可读取将会发生阻塞等待。