Socket基础:从零开始理解网络通信
各位同学,今天我们来聊聊Socket。说实话,Socket这个概念刚接触时确实有点抽象。我当年学的时候,脑子里全是问号——这玩意儿到底是个啥?
简单来说,Socket就是网络通信的"门"。你想跟另一台机器上的程序说话,就得先开一扇门。这扇门就是Socket。它屏蔽了底层网络协议的复杂性,让你能像读写文件一样进行网络通信。
Socket的概念与原理
Socket起源于Unix,设计哲学就是"一切皆文件"。网络通信本质上也是读写数据,只不过数据是在网络上传输,而不是在本地磁盘上。
我习惯把Socket理解成一个通信端点。两端各有一个Socket,通过IP地址和端口号来定位对方。就像你寄快递,IP地址是小区地址,端口号是具体门牌号。
核心要点:Socket是应用层与传输层之间的抽象接口。它封装了TCP/IP协议栈的细节,让开发者只需关注数据的收发。
通信流程其实很直观:服务端先创建Socket,绑定地址,然后监听。客户端创建Socket,连接服务端。连接建立后,双方就可以收发数据了。最后关闭连接。
Socket的分类
Socket有三种类型,每种对应不同的传输方式。我当年面试时就被问过这个,答不上来可尴尬了。
| 类型 | 名称 | 协议 | 特点 |
|---|---|---|---|
| SOCK_STREAM | 流式Socket | TCP | 面向连接、可靠、有序、无边界 |
| SOCK_DGRAM | 数据报Socket | UDP | 无连接、不可靠、有边界、效率高 |
| SOCK_RAW | 原始Socket | IP/ICMP等 | 直接操作网络层,可自定义协议 |
流式Socket(SOCK_STREAM):基于TCP,保证数据按顺序到达,不丢失。就像打电话——先建立连接,再通话,最后挂断。我在项目中做文件传输时,用的就是这种。
数据报Socket(SOCK_DGRAM):基于UDP,不保证可靠,但速度快。就像寄明信片——扔进邮筒就不管了,对方收不收得到看运气。做视频直播时,丢几帧画面无所谓,用UDP就对了。
原始Socket(SOCK_RAW):这个比较底层,可以直接构造IP包。我曾在做网络监控工具时用过,能抓到最原始的网络数据包。
我的建议:初学者先掌握流式Socket,这是最常用的。数据报Socket也建议学,很多高性能场景离不开它。原始Socket嘛,等你把前两个玩熟了再说。
Socket的地址结构:sockaddr_in
通信需要地址。在IPv4网络中,我们用sockaddr_in结构体来存放地址信息。这个结构体长这样:
struct sockaddr_in {
sa_family_t sin_family; // 地址族,AF_INET
in_port_t sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 填充,全0
};
嗯,这里有几个坑要注意。我刚开始写代码时,经常忘记把端口号转成网络字节序。用htons()函数转换一下就行。IP地址用inet_pton()把点分十进制转成二进制。
我曾经踩过的坑:sin_zero字段一定要清零。否则bind()可能失败,返回"Address already in use"。用memset(&addr, 0, sizeof(addr))就能搞定。
Socket的创建:socket()
创建Socket很简单,一个函数调用:
#include <sys/socket.h>
int sockfd = socket(int domain, int type, int protocol);
参数说明:
- domain:协议域,IPv4用
AF_INET,IPv6用AF_INET6 - type:Socket类型,
SOCK_STREAM或SOCK_DGRAM - protocol:协议,通常传0,让系统自动选择
举个例子,创建一个TCP Socket:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
返回的文件描述符就是Socket的句柄。之后所有操作都通过它来完成。
Socket的绑定:bind()
创建完Socket,服务端需要把它绑定到一个具体的地址和端口上。这样客户端才知道往哪连。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
我习惯这样写:
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
注意:客户端不需要调用bind(),系统会自动分配一个临时端口。只有服务端才需要显式绑定。
Socket的监听:listen()
绑定之后,服务端要告诉内核:我准备好了,可以接受连接了。这就是listen()做的事。
int listen(int sockfd, int backlog);
backlog参数指定连接队列的最大长度。什么意思呢?就是同时有多少个连接请求可以排队等待处理。
我一般设成5或10。设太大没意义,反而浪费资源。设太小的话,高并发时客户端会连不上。
if (listen(sockfd, 5) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
Socket的连接:connect() 和 accept()
这是通信双方建立连接的关键步骤。客户端调用connect(),服务端调用accept()。
客户端:connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端需要指定服务端的IP和端口:
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, "192.168.1.100", &server_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
服务端:accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()会阻塞,直到有客户端连接进来。它返回一个新的Socket描述符,用于和这个客户端通信。原来的监听Socket继续等待其他连接。
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd < 0) {
perror("accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
我的经验:accept()返回的client_fd才是真正用来收发数据的。原来的sockfd只负责监听。很多新手搞混这个,结果数据发不出去。
Socket的关闭:close() 和 shutdown()
通信结束,记得关闭Socket。这不仅是好习惯,更是必须的——否则会资源泄漏。
close():关闭Socket,释放文件描述符。调用后不能再读写。
close(sockfd);
shutdown():更精细的控制。可以只关闭读、只关闭写,或者都关闭。
int shutdown(int sockfd, int how);
// how: SHUT_RD(关闭读), SHUT_WR(关闭写), SHUT_RDWR(都关闭)
为什么要用shutdown?举个例子:你发完数据想告诉对方"我说完了",但还想接收对方的回应。这时用shutdown(sockfd, SHUT_WR),发送FIN包,但保留读通道。
我曾经踩过的坑:close()只是把文件描述符的引用计数减1。如果Socket被多个进程共享,要等所有进程都close()才会真正关闭。shutdown()则立即生效,不管引用计数。
好了,Socket的基础知识就讲到这里。这些是网络编程的基石,一定要亲手敲代码试试。光看是学不会的,我当年也是写了无数遍才真正理解。
公众号:蓝海资料掘金营,微信deep3321