4、Socket编程入门:Socket概念、Socket类型、Socket API介绍

说到网络编程,Socket 是绕不开的第一个坎。

我记得刚入行那会儿,看了一堆理论书,什么 OSI 七层、TCP/IP 四层,背得滚瓜烂熟。结果第一次写网络程序,打开 IDE 就懵了——这 Socket 到底是个啥?

说白了,Socket 就是操作系统提供的一个编程接口。它把复杂的网络协议栈封装起来,让你能像读写文件一样,往网络上发数据、收数据。你想想看,如果没有 Socket,你得自己去处理 TCP 的三次握手、滑动窗口、重传机制……那还写个锤子业务代码。

4.1 什么是 Socket?

Socket 这个词,直译是「插座」。这个比喻其实挺形象的。

你想啊,你家墙上的插座,插上电器就能通电。网络里的 Socket 也一样——你创建一个 Socket,绑定一个 IP 和端口,另一端连上来,数据就能流通了。

核心定义:Socket 是应用层与传输层之间的抽象层。它隐藏了 TCP/IP 协议的细节,让开发者可以专注于收发数据。

我在项目中遇到过一种情况:两个服务之间通信,偶尔会丢数据。查了半天,发现是 Socket 的缓冲区设置太小,数据还没读完就被覆盖了。嗯,这种坑踩过一次就记住了。

4.2 Socket 的类型

Socket 不是只有一种。根据传输层协议的不同,主要分三类:

类型 协议 特点 典型场景
SOCK_STREAM TCP 面向连接、可靠、有序、无边界 HTTP、FTP、SSH
SOCK_DGRAM UDP 无连接、不可靠、有边界、速度快 DNS、视频直播、游戏
SOCK_RAW IP 直接操作 IP 层,可自定义协议 网络嗅探、防火墙、Ping

平时用得最多的,就是 SOCK_STREAM(TCP)和 SOCK_DGRAM(UDP)。

我个人习惯是:需要保证数据不丢、不乱序的,用 TCP;对实时性要求高、能容忍少量丢包的,用 UDP。

小提示:TCP 虽然可靠,但它的「粘包」问题也是很多新手头疼的地方。这个我们后面会专门讲。

4.3 Socket API 核心函数

Python 的 socket 模块把底层的 C API 封装得很友好。我挑几个最常用的讲:

4.3.1 socket() —— 创建 Socket

import socket

# 创建一个 TCP Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 创建一个 UDP Socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

AF_INET 表示使用 IPv4 地址。如果你要用 IPv6,就换成 AF_INET6。

4.3.2 bind() —— 绑定地址和端口

s.bind(('0.0.0.0', 8888))

0.0.0.0 表示监听所有网卡。如果你只想监听本机,可以用 127.0.0.1。

注意:端口号范围是 0-65535,但 0-1023 是系统保留端口,需要 root 权限才能绑定。我曾经图省事绑了 80 端口,结果程序跑不起来,查了半天才发现是权限问题。

4.3.3 listen() —— 开始监听

s.listen(5)  # 最大等待连接数为 5

这个参数叫 backlog,表示内核为这个 Socket 排队的最大连接数。不是越大越好,设太大反而浪费资源。

4.3.4 accept() —— 接受连接

conn, addr = s.accept()
print(f'收到来自 {addr} 的连接')

accept() 会阻塞,直到有客户端连上来。它返回一个新的 Socket 对象 conn,专门用来和这个客户端通信。

4.3.5 connect() —— 发起连接

s.connect(('192.168.1.100', 8888))

客户端用这个函数去连服务端。如果连不上,会抛 ConnectionRefusedError。

4.3.6 send() / recv() —— 收发数据

# 发送
s.send(b'Hello, Server!')

# 接收
data = s.recv(1024)  # 最多接收 1024 字节

这里有个坑:send() 不一定能把数据一次性发完,它返回的是实际发送的字节数。所以严谨的写法是循环发送,直到所有数据都发出去。

避坑指南:我曾经写过一个文件传输程序,直接用 send() 发大文件,结果只发了一半。后来改成循环发送,问题才解决。

4.3.7 close() —— 关闭 Socket

s.close()

用完记得关。不关的话,端口会被占用,下次启动程序就会报 Address already in use。

4.4 一个完整的 TCP 回显服务器

光说不练假把式。我写个最简单的例子,你感受一下:

# server.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 8888))
s.listen(5)
print('服务器启动,等待连接...')

while True:
    conn, addr = s.accept()
    print(f'客户端 {addr} 已连接')
    data = conn.recv(1024)
    if data:
        conn.send(data)  # 原样返回
    conn.close()
# client.py
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))
s.send(b'Hello, Socket!')
data = s.recv(1024)
print(f'收到回复: {data.decode()}')
s.close()

先跑 server.py,再跑 client.py,你会看到客户端打印出「收到回复: Hello, Socket!」。

试试看:把 recv(1024) 改成 recv(5),看看会发生什么?这就是粘包拆包问题的雏形。

4.5 Socket 编程的核心流程

我把 TCP Socket 的完整生命周期画成了一张图,方便你理解:

服务器端 socket() bind() listen() accept() recv() / send() close() 客户端 socket() connect() send() / recv() close() 三次握手 数据传输 四次挥手

这张图把 TCP Socket 的完整流程串起来了。服务器端先创建 Socket,绑定地址,然后监听。客户端创建 Socket 后直接 connect。连接建立后,双方就可以收发数据了。最后,谁先 close,谁就发起挥手。

4.6 关于粘包拆包

嗯,这里要提一句。TCP 是流式协议,它没有消息边界。你发两次 send(),对方可能一次 recv() 就全收走了——这就是粘包。反过来,你发一次,对方可能要分多次 recv() 才能收完——这就是拆包。

这个问题我们后面会专门用一章来讲。你现在只要知道:TCP 不保证你发一次,对方就收一次。

重要:千万不要假设 send() 和 recv() 是一一对应的。我见过太多新手在这个问题上翻车了。

好了,Socket 入门就讲这么多。你先把上面的代码跑通,感受一下数据是怎么在网络上流动的。后面我们会一步步深入,把粘包拆包、协议设计这些硬骨头啃下来。

可可精品资料库