Python Socket与Linux Socket

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_INET6IPv6
socket.AF_UNIXUnix系统进程间通信
type
socket.SOCK_STREAM流式套接字,TCP
socket.SOCK_DGRAM数据报套接字,UDP
socket.SOCK_RAW原始套接字
// 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);

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 运行截图

  • 服务器
  • 客户端
12-17 00:45