上一篇文章简单介绍了什么是Socket以及如何用Socket实现客户端和服务端的基本通信。这一篇我打算详细拆解Socket的核心函数,配合代码示例让大家能真正动手写出一个可用的程序。
Socket数据结构
在正式讲函数之前,先看一下Socket编程中最核心的数据结构——sockaddr和sockaddr_in:
sockaddr是通用的地址结构,而sockaddr_in是IPv4专用的结构,实际编程中用得更多。两者可以通过强制类型转换互相转换。
Linux系统下的头文件
写Socket程序时,Linux下需要包含以下头文件:
#include<sys/socket.h>// 核心Socket函数和数据结构#include<netinet/in.h>// sockaddr_in结构和IP地址转换函数#include<arpa/inet.h>// inet_addr、inet_ntoa等地址转换函数#include<unistd.h>// close()函数#include<string.h>// memset()等字符串操作#include<errno.h>// 错误处理Windows系统下的头文件
Windows下的头文件略有不同:
#include<winsock2.h>// Windows Socket 2核心头文件#include<ws2tcpip.h>// IPv6支持和新的地址转换函数#pragmacomment(lib,"ws2_32.lib")// 链接Winsock库需要注意的是,Windows下使用Socket前必须先调用WSAStartup()初始化,结束时调用WSACleanup()清理。
服务端与客户端通信过程
先看一下完整的通信流程图,对整个过程有个全局概念:
服务端的流程是:创建socket → 绑定地址 → 监听 → 接受连接 → 读写数据 → 关闭连接。客户端则简单一些:创建socket → 连接服务器 → 读写数据 → 关闭连接。
基本函数
套接字的创建
socket()函数用于创建一个套接字描述符:
intsocket(intdomain,inttype,intprotocol);参数说明:
| 参数 | 含义 | 常用值 |
|---|---|---|
| domain | 协议族(地址族) | AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(本地通信) |
| type | 套接字类型 | SOCK_STREAM(TCP,面向连接)、SOCK_DGRAM(UDP,无连接)、SOCK_RAW(原始套接字) |
| protocol | 协议类型 | IPPROTO_TCP、IPPROTO_UDP,设为0时自动选择type对应的默认协议 |
socket()成功时返回非负的文件描述符,失败返回-1。
向套接字分配网络地址
bind()函数把一个地址绑定到socket上:
intbind(intsockfd,conststructsockaddr*addr,socklen_taddrlen);参数说明:
sockfd:socket()返回的文件描述符addr:指向sockaddr结构的指针,包含要绑定的IP地址和端口号addrlen:地址结构的长度
实际编程中通常用sockaddr_in结构来初始化,然后强制转换为sockaddr*:
structsockaddr_inserv_addr;memset(&serv_addr,0,sizeof(serv_addr));serv_addr.sin_family=AF_INET;// IPv4serv_addr.sin_addr.s_addr=INADDR_ANY;// 绑定所有可用网卡serv_addr.sin_port=htons(8888);// 端口号,需转换为网络字节序进入等待连接请求状态
服务端调用listen()开始监听:
intlisten(intsockfd,intbacklog);sockfd:要监听的socket描述符backlog:等待队列的最大长度,即未被accept的连接最大数量
客户端调用connect()发起连接:
intconnect(intsockfd,conststructsockaddr*addr,socklen_taddrlen);sockfd:客户端的socket描述符addr:服务器的地址结构addrlen:地址结构长度
接受客户端连接
服务端调用accept()接受连接请求:
intaccept(intsockfd,structsockaddr*addr,socklen_t*addrlen);sockfd:监听socket描述符addr:输出参数,用于获取客户端的地址信息addrlen:输入输出参数,调用前设为sizeof(struct sockaddr),返回实际地址长度
accept()成功时返回一个新的socket描述符,用于与该客户端通信;失败返回-1。注意,accept()是阻塞函数,如果没有连接请求会一直等待。
TCP三次握手
TCP协议通过三次握手建立可靠连接:
- 第一次握手:客户端发送SYN包(SYN=j),进入SYN_SEND状态
- 第二次握手:服务器收到SYN包,确认客户端的SYN(ACK=j+1),同时发送自己的SYN包(SYN=k),进入SYN_RECV状态
- 第三次握手:客户端收到SYN+ACK包,发送确认包ACK(ACK=k+1),双方进入ESTABLISHED状态
三次握手完成后,连接建立,可以开始传输数据。
TCP四次挥手
断开连接需要四次挥手:
由于TCP支持半关闭(half-close),一端可以在结束发送后继续接收数据。完整关闭需要四次握手:
- 主动关闭方发送FIN包,进入FIN_WAIT_1状态
- 被动关闭方收到FIN,发送ACK确认,进入CLOSE_WAIT状态
- 被动关闭方完成数据发送后,发送FIN包,进入LAST_ACK状态
- 主动关闭方收到FIN,发送ACK确认,进入TIME_WAIT状态,等待一段时间后完全关闭
发送数据
Linux下用send()或write()发送数据:
ssize_tsend(intsockfd,constvoid*buf,size_tlen,intflags);ssize_twrite(intsockfd,constvoid*buf,size_tcount);Windows下用send():
intsend(SOCKET s,constchar*buf,intlen,intflags);flags参数一般设为0,表示常规发送。成功时返回实际发送的字节数,失败返回-1。
接收数据
Linux下用recv()或read()接收数据:
ssize_trecv(intsockfd,void*buf,size_tlen,intflags);ssize_tread(intsockfd,void*buf,size_tcount);Windows下用recv():
intrecv(SOCKET s,char*buf,intlen,intflags);buf是接收缓冲区,len是缓冲区大小。成功时返回实际接收的字节数,返回0表示对方关闭了连接,失败返回-1。
关闭连接
Linux下用close()关闭socket:
intclose(intsockfd);Windows下用closesocket():
intclosesocket(SOCKET s);C代码实战
下面给出一个完整的TCP客户端和服务端示例,在Linux环境下编译运行。服务端接收客户端发送的消息并原样返回(echo服务)。
服务端代码 server.c
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/socket.h>#include<netinet/in.h>#definePORT8888#defineBUFFER_SIZE1024intmain(){intserver_fd,new_socket;structsockaddr_inaddress;intopt=1;intaddrlen=sizeof(address);charbuffer[BUFFER_SIZE]={0};// 创建socket文件描述符if((server_fd=socket(AF_INET,SOCK_STREAM,0))==0){perror("socket failed");exit(EXIT_FAILURE);}// 设置socket选项,允许端口复用if(setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt))){perror("setsockopt");exit(EXIT_FAILURE);}address.sin_family=AF_INET;address.sin_addr.s_addr=INADDR_ANY;address.sin_port=htons(PORT);// 绑定端口if(bind(server_fd,(structsockaddr*)&address,sizeof(address))<0){perror("bind failed");exit(EXIT_FAILURE);}// 开始监听,最大等待队列长度为3if(listen(server_fd,3)<0){perror("listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n",PORT);// 接受连接if((new_socket=accept(server_fd,(structsockaddr*)&address,(socklen_t*)&addrlen))<0){perror("accept");exit(EXIT_FAILURE);}printf("Client connected\n");// 接收客户端消息并原样返回intvalread;while((valread=read(new_socket,buffer,BUFFER_SIZE))>0){printf("Received: %s",buffer);send(new_socket,buffer,strlen(buffer),0);memset(buffer,0,BUFFER_SIZE);}if(valread==0){printf("Client disconnected\n");}else{perror("read failed");}close(new_socket);close(server_fd);return0;}客户端代码 client.c
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#definePORT8888#defineBUFFER_SIZE1024intmain(intargc,charconst*argv[]){intsock=0;structsockaddr_inserv_addr;charbuffer[BUFFER_SIZE]={0};charmessage[BUFFER_SIZE];// 创建socketif((sock=socket(AF_INET,SOCK_STREAM,0))<0){perror("socket creation failed");exit(EXIT_FAILURE);}serv_addr.sin_family=AF_INET;serv_addr.sin_port=htons(PORT);// 将IPv4地址从点分十进制转换为二进制if(inet_pton(AF_INET,"127.0.0.1",&serv_addr.sin_addr)<=0){perror("invalid address/ address not supported");exit(EXIT_FAILURE);}// 连接服务器if(connect(sock,(structsockaddr*)&serv_addr,sizeof(serv_addr))<0){perror("connection failed");exit(EXIT_FAILURE);}printf("Connected to server. Type 'exit' to quit.\n");while(1){printf("Enter message: ");fgets(message,BUFFER_SIZE,stdin);if(strncmp(message,"exit",4)==0){printf("Disconnecting...\n");break;}// 发送消息send(sock,message,strlen(message),0);// 接收服务器响应intvalread=read(sock,buffer,BUFFER_SIZE);printf("Server response: %s",buffer);memset(buffer,0,BUFFER_SIZE);}close(sock);return0;}编译与运行
# 编译服务端gcc server.c-oserver# 编译客户端gcc client.c-oclient# 先运行服务端./server# 打开另一个终端运行客户端./client这个示例比较简单,但涵盖了Socket编程的核心流程。实际项目中还需要考虑错误处理、并发处理、超时机制等问题。如果想支持多客户端并发,可以参考我后续的文章。