Linux学习笔记5:socket通信 1. 什么是 socketsocket套接字是 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 通信流程如下服务端客户端服务端客户端socket() 创建套接字bind() 绑定地址listen() 开始监听socket() 创建套接字connect() 请求连接accept() 接受连接write() 发送数据read() 接收数据close() 关闭连接close() 关闭连接UDP 通信则要简单很多不需要listen和accept直接用sendto和recvfrom发送和接收数据。3. 核心 API 详解Linux 提供了一组标准的 socket 系统调用所有函数都在sys/socket.h中声明。3.1socket()—— 创建套接字#includesys/socket.hintsocket(intdomain,inttype,intprotocol);domain地址族IPv4 用AF_INET本地通信用AF_UNIX。type套接字类型TCP 用SOCK_STREAMUDP 用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 服务端代码#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#includesys/socket.h#definePORT8080#defineBUFFER_SIZE1024intmain(){intserver_fd,client_fd;structsockaddr_inaddress;intaddrlensizeof(address);charbuffer[BUFFER_SIZE]{0};// 1. 创建 socketif((server_fdsocket(AF_INET,SOCK_STREAM,0))0){perror(socket failed);exit(EXIT_FAILURE);}// 2. 设置地址结构address.sin_familyAF_INET;address.sin_addr.s_addrINADDR_ANY;// 监听所有接口address.sin_porthtons(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_fdaccept(server_fd,(structsockaddr*)address,(socklen_t*)addrlen))0){perror(accept failed);exit(EXIT_FAILURE);}printf(Client connected.\n);// 6. 读取并回射ssize_tbytes_read;while((bytes_readread(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 客户端代码#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#includesys/socket.h#definePORT8080#defineBUFFER_SIZE1024intmain(){intsock0;structsockaddr_inserv_addr;charbuffer[BUFFER_SIZE]{0};char*messageHello from client;// 1. 创建 socketif((socksocket(AF_INET,SOCK_STREAM,0))0){perror(socket creation error);return-1;}// 2. 设置服务器地址serv_addr.sin_familyAF_INET;serv_addr.sin_porthtons(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 服务端#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#includesys/socket.h#definePORT8080#defineBUFFER_SIZE1024intmain(){intsockfd;structsockaddr_inserv_addr,cli_addr;socklen_tlensizeof(cli_addr);charbuffer[BUFFER_SIZE];sockfdsocket(AF_INET,SOCK_DGRAM,0);serv_addr.sin_familyAF_INET;serv_addr.sin_addr.s_addrINADDR_ANY;serv_addr.sin_porthtons(PORT);bind(sockfd,(structsockaddr*)serv_addr,sizeof(serv_addr));printf(UDP server listening on port %d\n,PORT);ssize_tnrecvfrom(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 客户端#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includearpa/inet.h#includesys/socket.h#definePORT8080#defineBUFFER_SIZE1024intmain(){intsockfd;structsockaddr_inserv_addr;socklen_tlensizeof(serv_addr);charbuffer[BUFFER_SIZE];char*msgHello UDP;sockfdsocket(AF_INET,SOCK_DGRAM,0);serv_addr.sin_familyAF_INET;serv_addr.sin_porthtons(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抓包分析网络问题。