Python网络编程之socket模块

socket模块

【一】概要

  • 在Python中,Socket模块是内置的标准库之一,它允许开发者使用Socket API进行网络通信。

  • Socket API(Application Programming Interface)是一组用于进行网络编程的接口函数,它定义了应用程序和操作系统或网络协议栈之间的通信规则。Socket API允许开发者通过调用这些接口函数来实现网络通信,包括建立连接、发送和接收数据等操作。

【二】常用方法

【1】TCP套接字编程

  • 服务端(server)
'''服务端'''
# 导入模块
import socket

# 定义端口和IP   # 一般设为常量
IP = '127.0.0.1'  # 回环地址   # 代表本地
PORT = 9999  # 端口  # 端口号最好超过8001 # 必须是整数类型

# 创建套接字对象
'''family默认参数【-1】为AF_INET网络协议,可以通过查看源码查看'''
'''type参数默认【-1】为流式协议,也就是TCP协议'''
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 上述等价于 socket.socket() 啥也不填,因为默认参数就是上面的,如有需要再修改参数即可

# 监听
server.bind((IP, PORT))
# 设置半连接池
server.listen()  # 默认为5

# 链接客户端
'''返回链接对象和链接对象的地址'''
conn, addr = server.accept()

# 接收信息
'''recv()默认为1024,含义是这一次接收1024个字节大小的'''
'''如果数据超过1024,将导致粘包问题,具体看详解'''
msg = conn.recv(1024)

# 发送数据
'''发送给客户端,必须是二进制数据'''
server.send(b'code')

# 关闭链接
conn.close()
# 关闭服务端
server.close()
  • 客户端(client)
'''客户端'''

import socket

IP = '127.0.0.1'  # 如果服务端IP改变,此处也要改变
PORT = 9999  # 与服务端保持一致

client = socket.socket()

# 链接服务端
client.connect((IP, PORT))

# 向服务端发送消息
client.send(b'')  # 必须是二进制数据
# 接收服务端消息
msg = client.recv(1024)  # 接收服务端的1024个字节的数据

# 断开链接
client.close()

【2】UDP套接字编程

  • 服务端
import socket

# 常量设置
IP = '127.0.0.1'
PORT = 8080

# 创建对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 报式协议  # 也就是UDP协议

# 监听
server.bind((IP, PORT))

# 接收信息
data, addr = server.recvfrom(1024)
print(data)

# 回复信息
server.sendto(b'server', addr)

# 关闭连接
server.close()
  • 客户端
import socket

# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送消息
client.sendto(b'client', (IP, PORT))
# 接收消息
data, addr = client.recvfrom(1024)
print(data)
# 关闭连接
client.close()

【三】详解

【1】socket抽象层

  • Socket(套接字)是一种提供网络通信的抽象层,它允许不同计算机上的应用程序通过网络进行通信。Socket为应用程序提供了一种通用的编程接口,使得程序员可以使用常见的套接字API进行网络通信,而不必关心底层网络细节。

image-20240118195433124

  • socket抽象层位于传输层和应用层之间

【1.1】Socket抽象层的一些关键特点和概念:

  1. 通信端点:
    • Socket定义了通信的两个端点,一个是发送数据的端点,另一个是接收数据的端点。
    • 通常,一个Socket用于表示一个端点,而一对Socket则形成了一个完整的通信连接。
  2. 协议族和协议类型:
    • Socket通过协议族(Protocol Family)和协议类型(Socket Type)来确定通信的方式和特性。
    • 常见的协议族包括IPv4、IPv6,常见的协议类型包括TCP、UDP。
  3. 套接字地址:
    • 每个Socket都有一个关联的套接字地址,用于标识其在网络中的位置。
    • 对于IPv4,套接字地址通常包含IP地址和端口号;对于IPv6,还包含了流标签等信息。
  4. Socket API:
    • Socket提供了一组通用的API,使得程序员能够方便地进行网络通信。这些API包括socket()、bind()、connect()、listen()、accept()等。
  5. 面向连接和无连接:
    • Socket可以基于连接(面向连接)或者不基于连接(无连接)进行通信。
    • 面向连接的Socket通常使用TCP协议,而无连接的Socket通常使用UDP协议。
  6. 服务器端和客户端:
    • 在Socket通信中,通信的一方通常是服务器端(提供服务),另一方是客户端(请求服务)。
    • 服务器端通常通过调用listen()等待连接请求,而客户端通常通过调用connect()请求连接。
  7. 阻塞和非阻塞:
    • Socket可以设置为阻塞模式或非阻塞模式,决定在进行某些操作时是否会阻塞程序执行。
  8. 多路复用:
    • Socket提供了多路复用的机制,允许一个进程同时处理多个Socket连接,提高程序的性能和效率。

【1.2】Socket API

  • Socket API(Application Programming Interface)是一组用于进行网络编程的接口函数,它定义了应用程序和操作系统或网络协议栈之间的通信规则。Socket API允许开发者通过调用这些接口函数来实现网络通信,包括建立连接、发送和接收数据等操作。

在常见的操作系统中,Socket API通常包括以下基本函数:

  1. socket():
    • 创建一个新的Socket对象,并返回其文件描述符或句柄。该函数指定了协议族(IPv4、IPv6)、协议类型(TCP、UDP)等参数。
  2. bind():
    • 将Socket与特定的地址和端口绑定,用于服务器端。
  3. listen():
    • 用于TCP服务器端,开始监听连接请求。
  4. accept():
    • 用于TCP服务器端,接受客户端的连接请求,返回新的Socket对象用于与客户端通信。
  5. connect():
    • 用于TCP客户端,与服务器建立连接。
  6. send() / recv():
    • 用于发送和接收数据,适用于面向连接的Socket。
  7. sendto() / recvfrom():
    • 用于发送和接收数据,适用于无连接的Socket(如UDP)。
  8. close():
    • 关闭Socket连接。

【2】套接字家族(Socket Family)

  • AF_INET / AF_INET6 : 网络编程中常用
  • AF_UNIX : 用于本地进程间通信

​ 套接字家族(Socket Family)是一组相关的协议和参数,用于指定套接字的地址格式和通信方式。套接字家族定义了套接字的地址结构以及与之相关的协议类型。在Socket编程中,常见的套接字家族包括:

  1. AF_INET(IPv4):
    • AF_INET表示IPv4套接字家族,用于指定IPv4地址格式。在这个家族中,套接字地址通常由一个IPv4地址和一个端口号组成。
  2. AF_INET6(IPv6):
    • AF_INET6表示IPv6套接字家族,用于指定IPv6地址格式。IPv6的套接字地址包括一个IPv6地址和一个端口号。
  3. AF_UNIX / AF_LOCAL(Unix域套接字):
    • AF_UNIX或AF_LOCAL表示Unix域套接字家族,用于本地进程间通信。在这个家族中,套接字地址通常是一个文件路径。
  4. AF_ISO(ISO协议):
    • AF_ISO表示ISO协议套接字家族,用于支持ISO协议。
  5. AF_NS(Xerox Network Systems协议):
    • AF_NS表示Xerox Network Systems协议套接字家族,用于支持Xerox Network Systems协议。

​ 这些套接字家族对应不同的协议和地址结构,开发者在创建套接字时需要选择合适的套接字家族。在Socket API中,通常使用socket.AF_INET表示IPv4套接字家族,使用socket.AF_INET6表示IPv6套接字家族,使用socket.AF_UNIXsocket.AF_LOCAL表示Unix域套接字家族。

【3】TCP套接字编程

【3.1】TCP套接字工作流程

image-20240118200204104

  • 服务端server
    • 创建套接字对象socket.socket()
    • 绑定地址server.bind()
    • 监听链接server.listen()
    • 接受链接server.accept(),返回客户端的套接字对象和客户端地址,一般命名为conn,addr = server.accept()
    • 接收数据conn.recv()【上图中的read()
    • 发送数据conn.send()【上图中的write()
    • 关闭链接conn.close(),服务器可以只关闭与客户端的连接,因为客户端可能不止一个
  • 客户端client
    • 创建套接字对象socket.socket()
    • 连接到服务端client.connect()
    • 发送数据client.send()【上图中的write()
    • 接收数据client.recv()【上图中的read()
    • 关闭于服务端的连接client.close()
【e.g.】打电话
  • 你(服务端)
    • 【1】你有手机socket()
    • 【2】你插上卡bind()
    • 【3】你等待接听listen()
    • 【7】接听对象打给你的电话recv()
    • 【8】回复对象send()
    • 【11】收到对象挂断电话,你也挂断电话close()
    • 【12】关闭手机
  • 对象(客户端)
    • 【4】对象有手机socket()
    • 【5】对象知道你的手机号connect()
    • 【6】对象给你打电话send()
    • 【9】收到回复recv()
    • 【10】对象挂断电话close(),关闭手机
  • 需要注意,打电话时,可以一方说很多话,另一方等待,但是在socket中,必须一条send对应一条recv,如果没有send直接recv,那么将一直处于等待状态
  • 并且挂断电话是双向的,可以客户端提出挂断,也可以服务端提出挂断
  • 但是,如果服务端挂断的电话,而客户端并没有挂断电话,将会报错

【3.2】基于TCP协议的套接字编程

【3.2.1】基本模板
  • 我懒狗,用的b"english"来发送的数据,如果是中文或者其他类型,需要进行编码

  • 服务端

'''服务端'''
# 导入模块
import socket

# 定义端口和IP   # 一般设为常量
IP = '127.0.0.1'  # 回环地址   # 代表本地
PORT = 9999  # 端口

# 创建套接字对象
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)

# 监听
server.bind((IP, PORT))
# 设置半连接池
server.listen()  # 默认为5

# 链接客户端
'''返回链接对象和链接对象的地址'''
conn, addr = server.accept()

# 接收信息
msg = conn.recv(1024)
print(f"【{addr}】{msg.decode('ascii')}")   # 【('127.0.0.1', 9565)】I'm client!
# 发送数据
conn.send(b"Receive the message from client!I'm server!")

# 关闭链接
conn.close()
# 关闭服务端
server.close()
  • 客户端
'''客户端'''

import socket

IP = '127.0.0.1'  # 如果服务端IP改变,此处也要改变
PORT = 9999  # 与服务端保持一致

client = socket.socket()

# 链接服务端
client.connect((IP, PORT))

# 向服务端发送消息
client.send(b"I'm client!")  # 必须是二进制数据
# 接收服务端消息
msg = client.recv(1024)  # 接收服务端的1024个字节的数据
print(msg.decode('ascii'))  # Receive the message from client!I'm server!
# 断开链接
client.close()
【3.2.2】通讯循环
  • 为了多次交流,使用循环进行持续交互!
# 服务端
import socket

IP = '127.0.0.1'
PORT = 8090

# 创建socket对象
server = socket.socket()
# 监听
server.bind((IP, PORT))
# 半连接池
server.listen()
conn, addr = server.accept()
# 连接
while True:
    msg = conn.recv(1024)
    msg = msg.decode('utf8')
    print(f"来自{addr}的消息:{msg}")
    if msg == 'q':
        conn.close()
        # 关闭此次连接后,为下一个客户端创建连接
        conn, addr = server.accept()
    else:
        send_msg = input("(server)请输入消息:")
        if len(send_msg) == 0:
            print("消息不能为空!")
            continue
        elif send_msg == 'q':
            conn.send(b'break connect')
            conn.close()
            break
        conn.send(send_msg.encode('utf8'))

server.close()
# 客户端
import socket

IP = '127.0.0.1'
PORT = 8090
client = socket.socket()
client.connect((IP, PORT))
while True:
    send_msg = input("(client)请输入消息:")
    if len(send_msg) == 0:
        print("消息不能为空!")
        continue
    client.send(send_msg.encode('utf8'))
    accept_msg = client.recv(1024)
    print(accept_msg.decode('utf8'))
    if accept_msg == b'break connect':
        # 服务端关闭连接
        break
    if send_msg == 'q':
        break
client.close()
【3.2.3】提高代码健壮性以及优化链接循环
  • 当客户端意外退出,如果不做异常捕获,将会导致服务端崩溃

  • 服务端

import socket

server = socket.socket()
IP = '127.0.0.1'
PORT = 8080

server.bind((IP, PORT))
server.listen(5)

while True:
    conn, addr = server.accept()
    while True:
        try:   # 当客户端发生意外断开,为了保证服务器运行,进行异常捕获并关闭客户端的链接
            # 接收信息
            msg = conn.recv(1024)
            msg = msg.decode('utf8')
            print(f"【{addr}】的消息:{msg}")
            # 回复信息
            send_msg = input("请输入回复消息:")
            send_msg = send_msg.encode('utf8')
            conn.send(send_msg)
            if msg == 'q':
                # 关闭客户端链接
                break
        except Exception as e:
            print(e)
            break
    conn.close()

server.close()
  • 客户端
# 客户端
import socket

IP = '127.0.0.1'
PORT = 8080
client = socket.socket()
client.connect((IP, PORT))
while True:
    send_msg = input("(client)请输入消息:")
    if len(send_msg) == 0:
        print("消息不能为空!")
        continue
    client.send(send_msg.encode('utf8'))
    accept_msg = client.recv(1024)
    print(f"【server】的消息:{accept_msg.decode('utf8')}")
    if send_msg == 'q':
        break
client.close()
【补】半连接池

​ 在网络编程中,"半连接池"通常指的是处理TCP半连接(SYN_RCVD状态)的一种机制。在TCP三次握手中,服务器在接收到客户端的SYN报文时,会创建一个半连接(半开放连接)状态,此时服务器会分配资源来处理该连接,但还未进行最终的ACK确认。

半连接池的目的是管理这些半连接状态,防止资源泄漏和恶意连接。以下是半连接池的一般工作流程:

  1. 接收SYN报文:
    • 服务器接收到客户端发送的SYN报文,创建一个半连接,将连接放入半连接池。
  2. 资源分配:
    • 为半连接分配资源,可能包括分配内存、创建数据结构等。
  3. 处理连接:
    • 进行一些处理,可能涉及业务逻辑的初步检查。
  4. 发送SYN+ACK报文:
    • 如果一切正常,服务器发送SYN+ACK报文,将连接状态从半连接改为已连接。
  5. 接收ACK报文:
    • 等待客户端发送ACK报文,完成TCP三次握手。
  6. 连接完全建立:
    • 一旦收到客户端的ACK报文,连接完全建立,服务器可以开始正常地进行数据传输。
  7. 超时处理和连接释放:
    • 如果在一定时间内没有收到客户端的ACK报文,可以考虑进行超时处理,释放半连接资源。
  • 简而言之,TCP协议通讯时,在同一时间只能于一位客户端通讯,其他客户端处于半连接状态。当客户端的半连接数量超过半连接池的最大容量时,将会拒绝客户端的连接请求。
  • 半连接池的数量在server.listen(int)阶段声明

【4】UDP套接字编程

【4.1】UDP套接字工作流程

img

  • 服务端server
    • 创建套接字对象socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    • 绑定地址server.bind()
    • 接收数据data,addr = server.recfromv(1024)
    • 【可选】发送数据server.sendto(b'',addr)
    • 关闭链接server.close()
  • 客户端client
    • 创建套接字对象socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    • 发送数据client.sendto(b'',(IP,PORT))
    • 接收数据data, addr = client.recvfrom(1024)
    • 关闭连接client.close()

【4.2】基于UDP协议的套接字编程

【4.2.1】通讯循环
  • 基本模板就是开头常用方法中的,不再做赘述

  • 服务端

import socket

# 常量设置
IP = '127.0.0.1'
PORT = 8080

# 创建对象
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 报式协议  # 也就是UDP协议

# 监听
server.bind((IP, PORT))

while True:
    # 接收信息
    data, addr = server.recvfrom(1024)
    print(data)
    if data == b'break':
        server.sendto(data, addr)
        break
    # 回复信息
    server.sendto(data.upper(), addr)
# 关闭连接
server.close()
  • 服务端
import socket

# 常量设置
IP = '127.0.0.1'
PORT = 8080
# 创建对象
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
    send_msg = input("【server will return x.upper()】input:").strip()
    # 发送消息
    client.sendto(send_msg.encode('utf8'), (IP, PORT))
    # 接收消息
    data, addr = client.recvfrom(1024)
    print(data.decode('utf8'))
    if data == b'break':
        break
# 关闭连接
client.close()
posted @ 2024-01-18 23:11  Lea4ning  阅读(147)  评论(0编辑  收藏  举报