目录
- Python Socket与Linux Socket
- 1. socket
- 1.1 socket()方法
- 2. TCP
- 2.1 bind()方法
- 2.2 listen()方法
- 2.3 connect()方法
- 2.4 accpet()方法
- 2.5 recv()与send()
- 3. UDP
- 3.1 sendto()
- 3.2 recvfrom()
- 4. close()
- 5. Python实现hello/hi的简单的网络聊天程序
- 5.1 server.py
- 5.2 client.py
- 5.3 运行截图
- 1. socket
Python Socket与Linux Socket
socket: Python的底层网络接口,一般情况程序员不需要接触到这个模块。有更多的高级模块,比如
requests
可以直接使用。本文章试图从Python的socket模块和linux socket api的角度来对Python实现网络通讯方式进行分析,提高对TCP,UDP通讯方式的理解。最后用Python实现一个hello/hi的简单的网络聊天程序。
1. socket
在Python中如果想要使用一个自己的网络应用层协议,或者说想使用纯原生TCP,UDP来实现通讯,就需要使用Python的socket
模块。
import socket
socket
模块提供了访问BSD套接字的接口。在所有现代Unix系统、Windows、macOS和其他一些平台上可用。
1.1 socket()方法
# 使用socket()方法返回一个socket对象
s = socket.socket([family[, type, proto, fileno]])
重要参数:
- family: 套接字家族,如ipv4,ipv6,unix系统进程间通信
- type: 套接字类型,如tcp,upd
参数 | 描述 |
---|---|
family | |
socket.AF_INET(默认) | IPv4 |
socket.AF_INET6 | IPv6 |
socket.AF_UNIX | Unix系统进程间通信 |
type | |
socket.SOCK_STREAM | 流式套接字,TCP |
socket.SOCK_DGRAM | 数据报套接字,UDP |
socket.SOCK_RAW | 原始套接字 |
socket方法与Linux Socket的
socket
函数对应
// socket(协议域,套接字类型,协议)
int socket(int domain, int type, int protocol);
通过s = socket.socket()
方法,得到了一个socket对象。
Python中的socket对象的成员方法,是对套接字系统调用的高级实现,往往比C语言更高级。
2. TCP
2.1 bind()方法
通常如果是服务器,需要绑定一个总所周知的地址用于提供服务,所以需要绑定一个(IP:PORT),客户端可以通过连接这个地址来获得服务。而客户端则直接通过连接,由系统随机分配一个端口号。
python中bind()方法传入一个地址和端口的元组
s.bind((host: str, port: int))
linux socket将套接字作为对象,传入一个套接字和地址结构体
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2.2 listen()方法
开始监听,backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数,默认为1
s.listen(backlog: int)
linux socket同样需要额外传入套接字参数
int listen(int sockfd, int backlog);
2.3 connect()方法
connect
方法是客户端用发起某个连接的,接受一个目标主机名和端口号的元组参数
# address -> (hostname, port)
s.connect(address)
# connect_ex是connect的扩展方法,不同在于返回错误代码,而不是抛出错误
s.connect_ex(address)
linux socket中,参数分别为客户端套接字socket描述符,服务器socket地址,socket地址长度
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2.4 accpet()方法
服务器依次调用socket()
, bind()
, listen()
后就会监听指定地址,客户端通过connect()
向服务器发起连接请求。服务器监听到请求后会调用accept()
函数接受请求。这样端与端的连接就建立好了
python中,accept()
方法阻塞进程,等待连接,返回一个新的套接字对象和连接请求者地址信息。
# accept() -> (socket object, address info)
s.accept()
linux socket中,第一个参数是服务器套接字描述符,第二个为一个地址指针,用于返回客户端协议地址,第三个参数是协议地址长度。如果连接成功,函数返回值为内核自动生成的一个全新描述符
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
服务器的监听套接字一般只创建一个,而accept函数会返回一个连接套接字,服务器与客户端之间的通信是在连接套接字上进行。每来一个服务请求新建一个连接套接字与请求者通信,而监听套接字只有一个,完成服务后对应的连接套接字就会被关闭。(可以理解成,监听套接字是专门的接线员,只负责将电话转接给别的部门)
2.5 recv()与send()
send(data[, flags]) ->count
发送数据到socket,发送前要将数据转换为utf-8的二进制格式。返回发送数据长度,因为网络可能繁忙,导致数据没有全部发送完毕,所以要再次对剩下的数据进行发送。
python还有一个sendall()
sendall(data[, flags])
作用是不停调用send()
函数,直到所有数据发送完毕
linux socket中的send()
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
send()
函数先检查协议是否正在发送缓冲区数据,等待协议发送完毕或则缓冲区已没有数据,那么send
比较sockfd缓冲区剩余空间大小和发送数据的len。- 如果len大于剩余空间大小,则等待协议发送缓冲中数据
- 若len小于剩余空间,则将buf中数据拷贝到剩余空间
- 若发送数据长度大于套接字发送缓冲区长度,则返回-1
python中,从已连接套接字读取数据的函数为recv()
s.recv(bufsize: int)
从套接字接受数据,如果没有数据到达套接字,将会阻塞直到来数据或则远程连接关闭。
如果远程连接关闭且数据已全部读取,则抛出一个错误。
linux socket也有读取数据函数recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv()
等待s发送缓冲区发送完毕- 检查套接字s的接受缓冲区,若协议正在接收数据,则等待接受完毕。
- 将接收缓冲区的数据拷到buf中,接受数据可能大于buf长度,所以需要多次调用
recv()
3. UDP
在无连接的情况下,端到端需要使用另外的数据发送和接受方式
3.1 sendto()
python中发送UDP数据,将数据data发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
s.sendto(data,address)
linux socket: 由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
该函数比send()函数多了两个参数,dest_addr表示目地机的IP地址和端口号信息,而addrlen是地址长度。
3.2 recvfrom()
s.recvfrom() -> (data, address)
接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收的数据,address是发送数据的套接字地址。
linux socket: recvfrom()的情况与sendto()类似,需要指针来存放发送数据的套接字地址
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
4. close()
python和linux socket都需要对套接字关闭
python:
s.close()
linux socket:
int close(int socketfd)
5. Python实现hello/hi的简单的网络聊天程序
5.1 server.py
#! /usr/bin/env python3
import socket
from threading import Thread
import traceback
HOST = "127.0.0.1"
PORT = 65432
def recv_from_client(conn):
try:
content = conn.recv(1024)
return content
except Exception:
return None
class ServiceThread(Thread):
def __init__(self, conn, addr):
super().__init__()
self.conn = conn
self.addr = addr
def run(self):
try:
while True:
content = recv_from_client(self.conn)
if not content:
break
print(f"{self.addr}: {content.decode('utf-8')}")
self.conn.sendall(content)
self.conn.close()
print(f"{self.addr[0]}:{self.addr[1]} leave.")
except Exception:
traceback.print_exc()
if __name__ == "__main__":
s = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen()
print("Repeater server started successfully.")
while True:
conn, addr = s.accept()
print(f"Connected from {addr}")
service_thread = ServiceThread(conn, addr)
service_thread.daemon = True
service_thread.start()
except Exception:
traceback.print_exc()
s.close()
5.2 client.py
#! /usr/bin/env python3
import socket
from threading import Thread
HOST = "127.0.0.1"
PORT = 65432
class ReadFromConnThread(Thread):
def __init__(self, conn):
super().__init__()
self.conn = conn
def run(self):
try:
while True:
content = self.conn.recv(1024)
print(f"\n({HOST}:{PORT}): {content.decode('utf-8')}\nYOUR:", end="")
except Exception:
pass
if __name__ == "__main__":
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
read_thread = ReadFromConnThread(s)
read_thread.daemon = True
read_thread.start()
while True:
content = input("YOUR:")
if content == "quit":
break
s.sendall(content.encode("utf-8"))
s.close()
5.3 运行截图
- 服务器
- 客户端
作者:SA19225176,万有引力丶
参考资料来源:USTC Socket网络编程