Linux学习笔记5:socket通信
1. 什么是 socket
socket(套接字)是 Linux 下网络通信的基石,它为不同主机(或同一主机)上的进程提供了一种双向通信的端点。可以把 socket 看作一根水管,一头连接你的程序,另一头连接远程程序或另一个本地进程,数据就在这根水管里流动。
在 Linux 中,socket 本质上是一个特殊的文件描述符,因此很多 I/O 操作(如read、write、close)同样适用于 socket,这也是 Unix/Linux “一切皆文件”哲学的体现。
常用的 socket 类型有:
- 流式套接字(SOCK_STREAM):基于 TCP,面向连接、可靠、有序的字节流。
- 数据报套接字(SOCK_DGRAM):基于 UDP,无连接、不可靠、保留消息边界。
- 原始套接字(SOCK_RAW):允许直接访问底层协议,常用于网络诊断工具。
在实际开发中,我们最常用的是 TCP 和 UDP 两种 socket。
2. socket 通信的基本流程
一个典型的 TCP 通信流程如下:
UDP 通信则要简单很多,不需要listen和accept,直接用sendto和recvfrom发送和接收数据。
3. 核心 API 详解
Linux 提供了一组标准的 socket 系统调用,所有函数都在<sys/socket.h>中声明。
3.1socket()—— 创建套接字
#include<sys/socket.h>intsocket(intdomain,inttype,intprotocol);domain:地址族,IPv4 用AF_INET,本地通信用AF_UNIX。type:套接字类型,TCP 用SOCK_STREAM,UDP 用SOCK_DGRAM。protocol:通常填 0,让系统根据前两个参数自动选择协议。- 返回值:成功返回文件描述符,失败返回 -1 并设置
errno。
3.2bind()—— 绑定地址
intbind(intsockfd,conststructsockaddr*addr,socklen_taddrlen);将创建的 socket 与一个本地地址(IP + 端口)绑定。服务端通常需要调用,客户端也可以绑定以指定源端口,但一般不这样做。
3.3listen()—— 开始监听(仅 TCP 服务端)
intlisten(intsockfd,intbacklog);将主动套接字变为被动套接字,准备接受客户端的连接。backlog指定已完成三次握手但还未被accept的连接队列的最大长度。
3.4accept()—— 接受连接(仅 TCP 服务端)
intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);从已完成连接队列中取出一个连接,并返回一个新的文件描述符,用于与这个客户端进行通信。addr会带回客户端的地址信息。
3.5connect()—— 发起连接(仅 TCP 客户端)
intconnect(intsockfd,conststructsockaddr*addr,socklen_taddrlen);客户端通过connect向服务端发起三次握手,建立连接。成功后即可在这条连接上收发数据。
3.6 数据收发
write()/read():与普通文件操作一样,适合简单的阻塞 I/O。send()/recv():增加了一些flags参数,可以设置非阻塞、发送带外数据等。sendto()/recvfrom():UDP 专用,需要在参数中指定对方的地址。
4. 一个完整的 TCP 回射服务端/客户端示例
下面用 C 实现一个简单的 TCP 回射(echo)服务器和客户端。服务器会原样返回客户端发来的任何数据,直到客户端关闭连接。
4.1 服务端代码
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>#definePORT8080#defineBUFFER_SIZE1024intmain(){intserver_fd,client_fd;structsockaddr_inaddress;intaddrlen=sizeof(address);charbuffer[BUFFER_SIZE]={0};// 1. 创建 socketif((server_fd=socket(AF_INET,SOCK_STREAM,0))==0){perror("socket failed");exit(EXIT_FAILURE);}// 2. 设置地址结构address.sin_family=AF_INET;address.sin_addr.s_addr=INADDR_ANY;// 监听所有接口address.sin_port=htons(PORT);// 端口号,host to network short// 3. 绑定地址if(bind(server_fd,(structsockaddr*)&address,sizeof(address))<0){perror("bind failed");exit(EXIT_FAILURE);}// 4. 开始监听if(listen(server_fd,3)<0){perror("listen failed");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n",PORT);// 5. 接受连接if((client_fd=accept(server_fd,(structsockaddr*)&address,(socklen_t*)&addrlen))<0){perror("accept failed");exit(EXIT_FAILURE);}printf("Client connected.\n");// 6. 读取并回射ssize_tbytes_read;while((bytes_read=read(client_fd,buffer,BUFFER_SIZE))>0){write(client_fd,buffer,bytes_read);memset(buffer,0,BUFFER_SIZE);}printf("Client disconnected.\n");close(client_fd);close(server_fd);return0;}4.2 客户端代码
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>#definePORT8080#defineBUFFER_SIZE1024intmain(){intsock=0;structsockaddr_inserv_addr;charbuffer[BUFFER_SIZE]={0};char*message="Hello from client";// 1. 创建 socketif((sock=socket(AF_INET,SOCK_STREAM,0))<0){perror("socket creation error");return-1;}// 2. 设置服务器地址serv_addr.sin_family=AF_INET;serv_addr.sin_port=htons(PORT);// 将 IP 地址从点分十进制转换为二进制if(inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr)<=0){perror("invalid address / address not supported");return-1;}// 3. 连接服务器if(connect(sock,(structsockaddr*)&serv_addr,sizeof(serv_addr))<0){perror("connection failed");return-1;}// 4. 发送数据并接收回显send(sock,message,strlen(message),0);printf("Sent: %s\n",message);read(sock,buffer,BUFFER_SIZE);printf("Echo: %s\n",buffer);close(sock);return0;}4.3 编译与运行
先将两个代码分别保存为server.c和client.c,然后使用gcc编译:
gcc server.c-oserver gcc client.c-oclient先启动服务端:
./server再在另一个终端运行客户端:
./client客户端会输出:
Sent: Hello from client Echo: Hello from client这样一个最基础的 TCP 回射程序就跑通了。
5. UDP 通信示例
UDP 不需要建立连接,流程更简洁。下面给出一个简单的 UDP 回射示例。
5.1 服务端
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>#definePORT8080#defineBUFFER_SIZE1024intmain(){intsockfd;structsockaddr_inserv_addr,cli_addr;socklen_tlen=sizeof(cli_addr);charbuffer[BUFFER_SIZE];sockfd=socket(AF_INET,SOCK_DGRAM,0);serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=INADDR_ANY;serv_addr.sin_port=htons(PORT);bind(sockfd,(structsockaddr*)&serv_addr,sizeof(serv_addr));printf("UDP server listening on port %d\n",PORT);ssize_tn=recvfrom(sockfd,buffer,BUFFER_SIZE,0,(structsockaddr*)&cli_addr,&len);buffer[n]='\0';printf("Received: %s\n",buffer);sendto(sockfd,buffer,n,0,(structsockaddr*)&cli_addr,len);close(sockfd);return0;}5.2 客户端
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<arpa/inet.h>#include<sys/socket.h>#definePORT8080#defineBUFFER_SIZE1024intmain(){intsockfd;structsockaddr_inserv_addr;socklen_tlen=sizeof(serv_addr);charbuffer[BUFFER_SIZE];char*msg="Hello UDP";sockfd=socket(AF_INET,SOCK_DGRAM,0);serv_addr.sin_family=AF_INET;serv_addr.sin_port=htons(PORT);inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr);sendto(sockfd,msg,strlen(msg),0,(structsockaddr*)&serv_addr,len);printf("Sent: %s\n",msg);recvfrom(sockfd,buffer,BUFFER_SIZE,0,(structsockaddr*)&serv_addr,&len);printf("Echo: %s\n",buffer);close(sockfd);return0;}6. 常见问题与调试技巧
- 端口被占用:如果
bind时报Address already in use,说明端口在上一次程序结束后仍处于TIME_WAIT状态,可设置SO_REUSEADDR选项解决。 - 防火墙拦截:排查
iptables或云安全组规则,确保开放对应端口。 - 连接拒绝(Connection refused):说明服务端还没启动或端口号不对。
- 阻塞问题:默认 I/O 是阻塞的,如果一方不发送数据,另一方会一直等待。可以通过
select、poll、epoll实现多路复用,或者将 socket 设为非阻塞模式。
调试工具推荐:
netstat -tlnp:查看监听端口。ss -tlnp:比 netstat 更高效。tcpdump/wireshark:抓包分析网络问题。
