Linux 网络编程入门:Socket 编程详解
Socket(套接字)编程是让不同设备上的进程实现网络通信的核心方法,本质就是操作系统提供了一套标准化的 API,让我们能通过代码控制'进程如何通过网络收发数据',全程不用关心底层网络硬件、协议细节,只需要按规则调用接口就行。
Linux 网络编程基础教程,涵盖端口号原理、IP 与端口组合、TCP 与 UDP 传输层区别、网络字节序(大小端)及转换接口。重点讲解 Socket 编程核心 API(socket、bind、connect 等),并通过完整的 UDP Echo Server 实战代码演示如何创建套接字、绑定地址、收发数据,帮助开发者理解跨设备进程间通信的本质。

Socket(套接字)编程是让不同设备上的进程实现网络通信的核心方法,本质就是操作系统提供了一套标准化的 API,让我们能通过代码控制'进程如何通过网络收发数据',全程不用关心底层网络硬件、协议细节,只需要按规则调用接口就行。
端口号说白了就是操作系统给网络进程分配的唯一标识,就像你家的门牌号。数据从网络过来,靠端口号才能精准找到要交给哪个程序处理,没它数据就成了'无家可归'的流浪包。
端口号不是凭空来的,内核会通过专门的数据结构管理端口和进程的关联,简单说就是维护一张'端口 - 进程'映射表,数据来了先查这张表,再递交给对应进程,保证不会发错对象。
PID(进程 ID)是进程在本机的'校内学号',只在这台机器上有用;端口号是进程在网络中的'身份证号',跨设备通信全靠它。这种设计让网络和本地进程标识解耦,哪怕进程的 PID 变了,只要端口号不变,网络通信就不受影响。
IP 地址负责定位网络中的设备(比如'北京市朝阳区 XX 小区'),端口号负责定位设备上的进程(比如'小区里的 101 室')。一次完整的网络通信,必须靠{源 IP,源 port,目的 IP,目的端口号}这四元组,才能唯一确定'谁给谁发数据'。
Socket(套接字)本质就是'IP 地址 + 端口号'的组合,是应用程序和网络打交道的'接口'——有了 Socket,进程才能和外部设备建立连接、传输数据,没它程序就是'闭门造车',没法和外界通信。
端口号是 16 位整数,范围 0~65535,不同区间有固定用途,避免端口占用冲突。
一个进程可以占用多个端口(比如一个服务同时监听 TCP 和 UDP 端口),但一个端口同一时间只能被一个进程占用,不然数据过来,内核都不知道该交给谁。
要是还觉得抽象,就用这个例子类比:西天 = 目的 IP(定位'如来'所在的设备);如来的莲台 = 目的端口(定位'如来'这个进程);东土大唐 = 源 IP;唐僧的行囊 = 源端口;Socket = 西天 + 莲台(或东土大唐 + 行囊),保证唐僧能精准找到如来,完成'数据传递'。
从这两张图能明确:传输层属于系统内核,我们要通过网络通信,就得调用它提供的 TCP/UDP 协议。
TCP 是'字节流'协议,写数据像往水管里放水,连续写就行,特别简单;读数据像接水,不知道啥时候接完,还得处理'粘包',比较麻烦。
既然 UDP 不可靠,为啥不都用 TCP?TCP 为了可靠,协议复杂、占资源多(还要建立连接、维护状态);UDP 简单、无连接、速度快,开发周期短,适合直播、语音通话这种'能容忍少量丢包,但要实时'的场景。
不同设备存储数据的方式不一样,就像有人从左写字,有人从右写字,网络通信必须统一'写字顺序',不然数据传过去就成了'乱码'。
记住:12 存在最下面的是大端(Big Endian,高位在前)。
市面上大小端设备都有,为了通信统一,规定:所有发往网络的数据,必须是大端(网络字节序)。发送方要把本机字节序转成网络字节序,接收方再转回来,这样就不会解析错了。
补充一个关键点:网络传输规则:先发低地址数据,后发高地址数据。
不用自己写转换逻辑,系统提供了现成的接口,直接调用就行:
#include <arpa/inet.h>
// 主机序转网络序 (Host to Network)
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
// 网络序转主机序 (Network to Host)
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
Socket 是操作系统给的网络编程接口,核心要记住:网络通信的本质,其实是'跨设备的进程间通信'。
Socket 编程的核心函数都在这张图里,是入门的基础:
// 创建 socket 文件描述符 (TCP/UDP,客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 开始监听 socket (TCP,服务器)
int listen(int sockfd, int backlog);
// 接收请求 (TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 建立连接 (TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 发送数据
int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
// 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
别被'网络通信'唬住,其实就是不同设备上的进程在说话。大家看这张图,展示的是 TCP 三次握手的过程,这本质就是两个设备上的进程,通过网络建立连接、准备通信的过程。说到底,网络通信就是跨设备的进程间通信(IPC),和本地用管道、共享内存通信的核心目的一致,只是媒介换成了网络。
Socket 分三类,但重点学网络套接字就行——学会网络套接字,本地套接字自然就会了,没必要单独花时间。
核心区别是通信范围——本地只能在同一台机器,网络能跨设备。新手千万别把'连接断开过程'和'套接字分类'混为一谈,这是很容易踩的坑!
为了兼容不同类型的套接字,系统设计了 sockaddr 基类结构,有点像 C++ 的'继承多态',网络接口会自动区分你是要本地通信还是网络通信。
sockaddr 结构的作用,是让内核知道你要用哪种方式通信:是 AF_INET 的网络通信,还是 AF_UNIX 的本地通信,内核会根据结构里的字段自动区分,不用我们手动指定。
这里有个小知识点:为啥不用 void*,非要用 C 语言模拟'基类'?一是可读性更高;二是设计这个结构的时候,void*还没普及。
光懂理论没用,动手写个最简单的 UDP 回显服务器,就能把前面的知识点串起来——客户端发啥,服务器就回啥,新手入门超合适。
先看整体文件组织,清晰的结构能少踩很多坑:
写个 Makefile,不用每次手动敲编译命令,一键搞定客户端和服务器:
.PHONY: all clean
all: udpclient udpserver
udpclient: udpClient.cpp
g++ -o $@ $< -std=c++17
udpserver: UdpServer.cc UdpServer.hpp
g++ -o $@ $^ -std=c++17
clean:
rm -f udpclient udpserver
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
class UdpServer {
public:
UdpServer();
~UdpServer();
void Init();
void Start();
private:
int _sockfd;
struct sockaddr_in _local;
};
#include "UdpServer.hpp"
#include <cstdlib>
UdpServer::UdpServer() : _sockfd(-1) {}
UdpServer::~UdpServer() {
if (_sockfd != -1) {
close(_sockfd);
}
}
void UdpServer::Init() {
// 1. 创建 socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
perror("socket error");
exit(1);
}
std::cout << "socket success, sockfd : " << _sockfd << std::endl;
// 2. 填充 sockaddr_in 结构体
memset(&_local, 0, sizeof(_local));
_local.sin_family = AF_INET;
_local.sin_port = htons(8080); // 端口转换为网络字节序
_local.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地 IP
}
void UdpServer::Start() {
// 3. 绑定 socket 信息
if (bind(_sockfd, (struct sockaddr*)&_local, sizeof(_local)) < 0) {
perror("bind error");
exit(1);
}
std::cout << "Bind success" << std::endl;
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
// 4. 循环接收并回显
while (true) {
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n > 0) {
buffer[n] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 回显数据
sendto(_sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, addr_len);
} else if (n == 0) {
break;
} else {
perror("recvfrom error");
}
}
}
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket error");
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
char buffer[1024];
std::string msg;
std::cout << "Enter message: ";
std::getline(std::cin, msg);
strcpy(buffer, msg.c_str());
// 发送数据
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
// 接收回显
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n > 0) {
buffer[n] = '\0';
std::cout << "Echo: " << buffer << std::endl;
}
close(sockfd);
return 0;
}
socket 是创建 UDP 套接字的核心函数,其作用是为进程间通信创建一个'端点'。在 UDP 场景下,调用它时需指定三个关键参数:
函数执行后,会返回一个套接字描述符(类似文件描述符),后续的绑定、收发数据等操作都需要通过这个描述符来完成,是 UDP 网络通信的基础入口。
存储网络地址的结构体,是 sockaddr 的'子类',必须掌握:
系统提供的宏,能让字节序转换更简洁,不用自己写繁琐的转换逻辑:htons, htonl, ntohs, ntohl。
##是宏定义中的'符号连接运算符',可以将其两侧的符号拼接为一个新的标识符,用于从分离的文本片段创建自定义标识。这在预处理阶段非常有用,例如日志宏中拼接文件名和行号。
示例:
#define LOG(level, msg) std::cout << level << ": " << msg << std::endl;
Socket 使用的完整片段:
LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;
// 2. 绑定 socket 信息,ip 和端口,ip(比较特殊,后续解释)
// 2.1 填充 sockaddr_in 结构体
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 端口转换为网络字节序
local.sin_port = htons(_port);
// IP 转换为网络字节序(将字符串格式的 IP 转为 4 字节网络序)
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 调用 bind 绑定 Socket 与 IP、端口
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online