1.服务器
socket -> bind -> listen -> accept -> recv -> close
此篇意在用服务器类比于邮政系统,来进行服务器搭建流程函数的理解,若有需求,务必简单浏览上一篇对于此类比的介绍。
1.1socket——建立套接字(获取设立邮局法律授权,取得营业凭证)
#include sys/socket.h
int socket(int domain,int type,int protocol);
功能:站在内核的角度打开网络功能,并且可以产生可以使用网络通信的东西
返回值:成功返回套接字sockfd,相当于法律上邮局的授权;失败返回-1;
参数列表:
domain:协议簇
//相当于所申请的邮局需要指定邮局服务的地理范围
IPV4:AF_INET
IPV6:AF_INET6
type:服务器的类型 :
//相当于明确邮局提供的服务种类,是要慢一点但是有追踪保障的(UDP),还是火速但是有丢失风险的(TCP)。
TCP: SOCK_STREAM
UDP: SOCK_DGRAM
protocol:额外协议
不需要额外协议写0
通过这个类比,我们可以更清晰地理解 socket()
函数的作用:它是创建网络通信端点(套接字)的第一步,为服务器在网络上提供服务打下基础。这个过程需要明确通信的细节,如服务范围、服务种类和具体协议,以确保后续通信的顺利进行。
1.2 bind——绑定套接字(取得授权后,施工建立邮局)
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen)
功能:
给套接字绑定好IP和端口等信息
//相当于选择好邮局具体的地址,与开设具体的服务窗口。以便于未来有顾客(客户端)有需求时可以找到这个邮局及其具体的窗口。
参数列表:
sockfd:创建好的套接字//施工时拿出你的授权
addr:一个指向
sockaddr
结构的指针,该结构包含了套接字需要绑定的地址信息。addrlen:结构体大小
其中addr常用新版结构体:
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]; // 填充,保证sockaddr结构的大小一致
};
- sin_family: 地址族,对于 IPv4 地址,这个字段通常设置为
AF_INET
。- sin_port: 端口号,以网络字节序表示,即大端序。通常使用
htons()
函数进行转换。- sin_addr: 结构体,包含一个 IPv4 地址,也是以网络字节序表示。可以使用
inet_addr()
或inet_pton()
函数进行设置。
- sin_addr.s_addr: IPv4 地址的无符号整数表示,以网络字节序存储。
- sin_zero: 一个填充数组,用于确保
sockaddr_in
结构体的大小与更通用的sockaddr
结构体一致。在大多数现代系统中,这个填充是不必要的,因为sockaddr_in
结构体的大小已经被调整为与sockaddr
一致。
1.3 listen——监听套接字(监听邮件收发服务请求)
int listen(int sockfd, int backlog);
功能:
监听套接字是否有连接,确立监听队列的大小
返回值:
成功返回0,失败返回-1
参数列表:
sockfd:已经绑定好信息的套接字
backlog:队列大小 2*backlog+1
1.4accept——接收套接字(接收并处理用户请求)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:这是监听套接字的文件描述符。服务器使用这个文件描述符来等待客户端的连接请求。
addr:这是一个可选参数,指向
sockaddr
结构的指针,该结构用于接收连接客户端的地址信息。如果不需要客户端地址,可以设置为NULL
。addrlen:这是一个指向
socklen_t
变量的指针,该变量在调用accept
之前应该被初始化为addr
所指向结构的长度。accept
函数可能会修改这个长度,以反映实际接收到的地址长度。如果不需要客户端地址,这个参数也可以设为NULL
。
1.5recv——处理请求
(这个和邮局点不一样,这里相当于邮局会对顾客的邮件进行处理,也许是读取,也许是完成你的需求,相当于升级的更加多功能的邮局吧)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd: 已连接的套接字的文件描述符。
- buf: 指向一个缓冲区的指针,用于存储接收到的数据。
- len: 缓冲区的长度,即可以接收的数据的最大字节数。
- flags: 用于指定接收操作的特殊选项。常用的标志有:
0
: 正常接收数据。MSG_PEEK
: 窥视接收队列中的数据,但不从队列中移除。MSG_WAITALL
: 等待直到接收到len
个字节的数据。- 成功时,
recv
函数返回接收到的字节数,它可以小于len
指定的缓冲区大小。- 如果对方关闭了连接,并且没有更多数据可接收,返回0。
- 出错时,返回
-1
,并设置全局变量errno
以指示错误类型。
1.6close——关闭(相当于一次需求的处理完成)
int close(int fd);
- fd: 要关闭的文件描述符(或套接字描述符)。
调用
close
函数时,如果出现错误,它会返回-1
。常见的错误原因包括:
EBADF
:提供的文件描述符fd
无效或未打开。EINTR
:关闭操作被中断,通常由于接收到信号。
2.客户端
socket -> bind(可选) -> connect -> send/recv -> close
这里和服务器的搭建相比基本相似,可参考服务器来理解。
3.完整示例
3.1服务器代码
下面是一个使用TCP协议的简单C语言服务器端程序的示例,它遵循了 socket -> bind -> listen -> accept -> send/recv -> close
的流程:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 // 服务器监听的端口号 #define BACKLOG 5 // 允许的最大等待连接数 int main() { int server_fd, client_fd; // 服务器和客户端的套接字文件描述符 struct sockaddr_in server_addr, client_addr; // 服务器和客户端的地址结构 socklen_t client_len = sizeof(client_addr); // 客户端地址结构的大小 char buffer[1024] = {0}; // 数据接收缓冲区 int ret; // 创建套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("Cannot create socket"); exit(EXIT_FAILURE); } // 设置服务器地址参数 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // 使用IPv4地址 server_addr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用接口 server_addr.sin_port = htons(PORT); // 端口号 // 将套接字绑定到服务器地址 if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); exit(EXIT_FAILURE); } // 开始监听传入连接 if (listen(server_fd, BACKLOG) < 0) { perror("Listen failed"); exit(EXIT_FAILURE); } // 无限循环,接受连接 while (1) { printf("Waiting for incoming connections...\n"); // 接受一个连接(会阻塞,直到一个客户端连接到服务器) client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("Accept failed"); exit(EXIT_FAILURE); } // 打印客户端的IP地址和端口号 printf("Connection accepted from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); // 通信示例:从客户端接收数据,然后发送数据 while ((ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) { // 发送收到的数据 send(client_fd, buffer, ret, 0); } if (ret < 0) { perror("Recv failed"); } // 关闭客户端套接字 close(client_fd); } // 关闭服务器套接字 close(server_fd); return 0; }
3.2客户端代码
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 8080 // 服务器监听的端口号 #define SERVER "127.0.0.1" // 服务器的IP地址 int main() { int sockfd; // 套接字文件描述符 struct sockaddr_in serv_addr; // 服务器的地址结构 char buffer[1024] = {0}; // 数据接收缓冲区 // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("Cannot create socket"); exit(EXIT_FAILURE); } // 设置服务器地址参数 memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; // 使用IPv4地址 serv_addr.sin_port = htons(PORT); // 端口号 serv_addr.sin_addr.s_addr = inet_addr(SERVER); // 服务器IP地址 // 连接到服务器 if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("Connect failed"); close(sockfd); exit(EXIT_FAILURE); } printf("Connected to the server.\n"); // 发送消息到服务器 const char *message = "Hello, Server!"; int message_len = strlen(message) + 1; int ret = send(sockfd, message, message_len, 0); if (ret < 0) { perror("Send failed"); close(sockfd); exit(EXIT_FAILURE); } printf("Message sent.\n"); // 接收服务器的回显响应 ret = recv(sockfd, buffer, sizeof(buffer) - 1, 0); if (ret < 0) { perror("Recv failed"); close(sockfd); exit(EXIT_FAILURE); } else if (ret == 0) { printf("The server closed the connection.\n"); } else { // 添加字符串结束符,并打印接收到的数据 buffer[ret] = '\0'; printf("Received: %s\n", buffer); } // 关闭套接字 close(sockfd); return 0; }
4.后续
进行基本UDP服务器与客户端的搭建流程示例。