目录
前言
1、编程环境
编码所用IDE:VScode 1.87.0
编译工具:gcc version 11.4.0
运行环境:
1、Windows Subsystem for Linux (WSL) 2
2、Ubuntu 22.04.4 LTS
2、编译命令
如无特别说明,通用编译命令为:
gcc -o server -g -Wall -std=gnu99 filename.c
或
gcc -o client -g -Wall -std=gnu99 filename.c
3、标题名前缀解释
XXX1或XXX2中,XXX代表一个主题,凡是具有相同的XXX,都是同一主题。
1代表服务端内容(代码),2代表客户端内容(代码)。
4、注意事项
所有代码都需要先运行服务端,而后再运行客户端,或者先运行接收端,再运行发送端。
0111UDP的回声接收端
注意!此部分内容需要先启动接收端,而后再启动发送端。
编译命令:
gcc -o receive -g -Wall -std=gnu99 0111UDP的回声接收端.c
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库,提供多种函数
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换函数库
#include <sys/socket.h> // 引入套接字API
#include <netinet/in.h> // 引入网络结构体定义
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节
int main(int argc, char const *argv[]) // 程序入口,argc为参数数量,argv为参数数组
{
int serv_sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建UDP套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_port = htons(1234); // 设置端口号为1234,htons用于网络字节序转换
// 利用 INADDR_ANY 自动获取 IP 地址,htonl 函数进行转换
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置IP地址为本地任意地址
bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 绑定套接字到本地地址
struct sockaddr_in clnt_addr; // 定义客户端地址结构体
socklen_t clnt_addr_size = sizeof(clnt_addr); // 定义客户端地址结构体大小变量
memset(&clnt_addr, 0, sizeof(clnt_addr)); // 初始化客户端地址结构体为0
char buffer[BUF_SIZE]; // 定义缓冲区数组
while (1) // 无限循环,等待客户端数据
{
memset(buffer, 0, BUF_SIZE); // 清空缓冲区
int receive_data_size = recvfrom(serv_sock, buffer, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
// 从客户端接收数据到缓冲区,并获取客户端地址信息
printf("发送端传来的数据:%s\n", buffer); // 打印接收到的数据
sendto(serv_sock, buffer, receive_data_size, 0, (struct sockaddr *)&clnt_addr, clnt_addr_size);
// 将接收到的数据发送回客户端
if (strcmp(buffer, "exit") == 0) // 如果接收到的数据为"exit"
{
break; // 退出循环
}
}
close(serv_sock); // 关闭套接字
printf("关闭连接。\n"); // 打印关闭连接信息
return 0; // 程序结束
}
0112UDP的回声发送端
编译命令:
gcc -o send -Wall -g -std=gnu99 0112UDP的回声发送端.c
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库,提供多种函数
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换函数库
#include <sys/socket.h> // 引入套接字API
#include <netinet/in.h> // 引入Internet地址族
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节
int main(int argc, char const *argv[]) // 主函数,argc为参数数量,argv为参数数组
{
int clnt_sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建一个UDP套接字
struct sockaddr_in serv_addr; // 定义一个服务器地址结构
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_port = htons(1234); // 设置端口号为1234,转换为网络字节序
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址为本地环回地址
struct sockaddr_in from_addr; // 定义一个来源地址结构
socklen_t from_addr_size = sizeof(from_addr); // 定义来源地址结构大小
memset(&from_addr, 0, sizeof(from_addr)); // 初始化来源地址结构为0
char buffer[BUF_SIZE]; // 定义一个字符数组作为缓冲区
while (1) // 无限循环
{
memset(buffer, 0, BUF_SIZE); // 清空缓冲区
printf("请输入需要发送给接收端的内容:"); // 提示用户输入
scanf("%s", buffer); // 从标准输入读取字符串到缓冲区
sendto(clnt_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 向服务器发送数据
memset(buffer, 0, BUF_SIZE); // 清空缓冲区
int receive_data_size = recvfrom(clnt_sock, buffer, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size); // 从服务器接收数据
buffer[receive_data_size] = '\0'; // 在接收到的数据末尾添加字符串结束符
printf("接收到接收端的数据为:%s\n", buffer); // 打印接收到的数据
if (strcmp(buffer, "exit") == 0) // 如果接收到的数据是"exit"
{
break; // 退出循环
}
}
close(clnt_sock); // 关闭套接字
printf("关闭连接。\n"); // 打印关闭连接的信息
return 0; // 程序结束
}
运行结果:
切记!需要先启动接收端,而后再启动发送端
0120HTTP服务器
在编写HTTP服务器的代码之前,首先需要准备2个文件 :1个是home.html,这是客户端浏览器所需要访问的文件,另一个是error.html,这是客户端请求不到文件时,服务器所返回的错误页面文件,这两个HTML文件代码如下:
home.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>首页</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f8f9fa;
}
.home-container {
text-align: center;
}
.home-title {
font-size: 2rem;
font-weight: bold;
color: #007bff;
}
.home-description {
font-size: 1.25rem;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="home-container">
<div class="home-title">欢迎来到我的网站</div>
<div class="home-description">这是一个简单的首页示例。</div>
</div>
</body>
</html>
error.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>错误页面</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f8f9fa;
}
.error-container {
text-align: center;
}
.error-code {
font-size: 5rem;
font-weight: bold;
color: #dc3545;
}
.error-message {
font-size: 1.5rem;
margin-top: 1rem;
}
.back-home {
display: inline-block;
margin-top: 2rem;
padding: 0.5rem 1rem;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 0.25rem;
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-code">404</div>
<div class="error-message">很抱歉,您访问的页面不存在。</div>
<a href="./home.html" class="back-home">返回首页</a>
</div>
</body>
</html>
这两个文件保存在与HTTP服务器程序相同的当前文件夹下即可,如下图所示。
http服务器的代码:
#include <stdio.h> // 包含标准输入输出库
#include <string.h> // 包含字符串操作库
#include <stdlib.h> // 包含标准库,如system调用
#include <unistd.h> // 包含Unix标准函数,如read、write等
#include <arpa/inet.h> // 包含IP地址转换函数
#include <sys/socket.h> // 包含套接字相关的函数
#include <netinet/in.h> // 包含网络结构体定义
#include <pthread.h> // 包含线程相关的函数
#define BUFFLEN 1024 // 定义缓冲区大小
#define SERVER_PORT 1234 // 定义服务器端口号
#define HTTP_FILENAME_LEN 256 // 定义 HTTP 文件名长度
#define SUFFIX_BUFF_SIZE 16 // 存放后缀名缓冲区的大小
// 定义文件类型结构体,用于映射文件后缀到 MIME 类型
struct doc_type
{
char *suffix; // 文件后缀
char *type; // MME类型
};
// 返回内容对应 MIME 类型
struct doc_type file_type[] =
{
{"html", "text/html"},
{"ico", "image/x-icon"},
{NULL, NULL}};
// HTTP 响应头模板
char *http_res_hdr_tmpl = "HTTP/1.1 200 OK\nServer: orange\n"
"Accept-Ranges: bytes\nContent-Length: %d\nConnection: closed\n"
"Content-Type: %s\n\n";
/**
* @brief 解析HTTP请求中的命令,提取文件名和文件后缀名
*
* 这个函数从给定的HTTP请求缓冲区中解析出请求的文件名和文件后缀名。
* 它假设请求行的格式是正确的,并且包含一个完整的HTTP请求行。
*
* @param buf 包含HTTP请求行的缓冲区
* @param file_name 用于存储提取出的文件名的缓冲区
* @param suffix 用于存储提取出的文件后缀名的缓冲区
*
* @note 这个函数没有进行错误检查,因此在格式错误的请求行上调用它可能会导致未定义的行为。
*/
void http_parse_request_cmd(char *buf, char *file_name, char *suffix)
{
// 查找 URL 开始的位置,空格后面紧跟URL,所以需要+1
char *begin = strchr(buf, ' ');
begin += 1;
// 查找 URL 结束的位置,并令URL结束后的一个空格置0
char *end = strchr(begin, ' ');
*end = 0;
/* 获取请求文件的文件路径 */
// 文件路径长度
int file_length = end - begin - 1;
// 将文件路径复制到 file_name 中, begin + 1是真实的文件路径起始位置
memcpy(file_name, begin + 1, file_length);
// 文件路径末尾添加结束符
file_name[file_length] = 0;
/* 获得文件后缀名 */
char *bias = strrchr(begin, '/');
// 计算最新文件路径的长度
int suffix_length = end - bias;
// 提取出带文件后缀名的文件名
if (*bias == '/')
{
++bias;
--suffix_length;
}
// 找到带文件后缀名的文件名且长度大于0的情况下
if (suffix_length > 0)
{
// 找到文件后缀名的位置
begin = strchr(file_name, '.');
// 如果找到了,则将'.'之后的字符串复制到保存文件后缀名的缓冲区中
if (begin)
{
strcpy(suffix, begin + 1);
}
}
}
/**
* @brief 根据文件后缀名获取对应的HTTP内容类型(MIME类型)
*
* 这个函数通过比较输入的文件后缀名与预定义的文件类型数组,
* 来确定相应的HTTP内容类型(MIME类型)。如果找到匹配的文件后缀名,
* 则返回对应的MIME类型;如果没有找到,则返回NULL。
*
* @param suffix 输入的文件后缀名
*
* @return 如果找到匹配的MIME类型,则返回该类型的字符串指针;否则返回NULL。
*/
char *http_get_type_by_suffix(const char *suffix)
{
// 遍历文件类型数组,查找与输入后缀名匹配的MIME类型
for (struct doc_type *type = file_type; type->suffix != NULL; type++)
{
// 如果找到匹配的文件后缀名,返回对应的MIME类型
if (strcmp(type->suffix, suffix) == 0)
{
return type->suffix;
}
}
// 如果没找到匹配的文档类型则返回NULL;
return NULL;
}
/**
* @brief 线程函数,用于处理客户端的HTTP请求并响应
*
* 这个函数是一个线程的执行体,它被设计用来处理客户端的HTTP请求。
* 函数首先分离当前线程,使得线程结束后资源能够被系统自动回收。
* 然后,它从线程参数中获取客户端的套接字,并读取客户端发送的HTTP请求内容。
* 接着,函数解析HTTP请求以获取请求的文件路径和文件后缀名,并根据文件后缀名确定MIME类型。
* 函数会尝试打开请求的文件,如果文件不存在或者无法打开,则会返回一个错误页面。
* 然后,函数构造HTTP响应头部并发送给客户端,随后发送文件数据。
* 在所有数据发送完毕后,函数会关闭文件和套接字,并读取任何客户端发来的剩余数据。
*
* @param args 一个指向整数(客户端套接字)的指针
*
* @return 这个函数不返回任何值
*/
void *thread_func(void *args)
{
// 线程分离,给系统托管子线程,子线程结束后由系统回收占用资源
pthread_detach(pthread_self());
int clnt_sock = *(int *)args; // 从参数中获取客户端的套接字
char buff[BUFFLEN] = {0}; // 定义缓冲区用于读取和发送数据
// 读取客户端发送过来的HTTP请求内容
int num = read(clnt_sock, buff, sizeof(buff));
if (num > 0)
{
// 存储文件路径和文件后缀名的缓冲区
char file_name[HTTP_FILENAME_LEN] = {0}, suffix[SUFFIX_BUFF_SIZE] = {0};
// 解析HTTP请求,获取请求的文件路径和后缀
http_parse_request_cmd(buff, file_name, suffix);
// 根据文件后缀名获取对应的MIME类型
char *type = http_get_type_by_suffix(suffix);
int fp_type = 1; // 根据是否获取到MIME类型设置对应的标志位
int fp_has = 1; // 文件是否存在标志
FILE *fp; // 打开对应文件的文件指针
if (type == NULL)
{
fp_type = 0; // 文件不存在则置0
printf("访问的文件不存在!\n");
type = http_get_type_by_suffix("html");
fp = fopen("./error.html", "rb"); // 打开错误页面
}
else
{
fp = fopen(file_name, "rb"); // 尝试打开请求的文件
if (fp == NULL)
{
fp_has = 0; // 打开失败则置0
fp = fopen("./error.html", "rb"); // 打开错误页面
}
}
// 文件指针移动到文件末尾并返回文件所在位置可知文件有多大
fseek(fp, 0, SEEK_END);
int file_len = ftell(fp);
fseek(fp, 0, SEEK_SET);
// 存储HTTP响应头部
char http_header[BUFFLEN] = {0};
// 构造HTTP响应头部并计算HTTP响应头部长度
int hdr_len = sprintf(http_header, http_res_hdr_tmpl, file_len, type);
// 向客户端发送HTTP的响应头部
write(clnt_sock, http_header, hdr_len);
// 输出当前服务器的处理状态
if (fp_type == 1)
{
if (fp_has == 0)
{
printf("服务器不存在 %s 文件\n", file_name);
}
else
{
printf("%s 文件发送中……", file_name);
}
}
int nCount = 0; // 存储每次读取的字节数
memset(buff, 0, BUFFLEN);
// 向客户端发送文件数据
while ((nCount = fread(buff, 1, BUFFLEN, fp)) > 0)
{
// 发送数据,发送完毕后清空缓冲区
write(clnt_sock, buff, nCount);
memset(buff, 0, BUFFLEN);
}
fclose(fp); // 关闭文件
shutdown(clnt_sock, SHUT_RDWR); // 关闭连接
read(clnt_sock, buff, sizeof(buff)); // 读取一些客户端发来的剩余数据,比如TCP四次挥手最后一次的ACK包
close(clnt_sock); // 关闭套接字
}
return NULL;
}
/**
* @brief 处理服务器套接字的连接请求
*
* 这个函数用于监听服务器套接字,接受客户端的连接请求,并为每个新的连接创建一个线程来处理。
* 函数无限循环,等待并接受新的连接。当接受到一个连接后,它创建一个新的线程来处理该连接,
* 并将客户端的套接字传递给线程函数。如果线程创建失败,函数将输出错误信息并终止进程。
*
* @param serv_sock 服务器套接字
*/
void handle_connect(int serv_sock)
{
while (1)
{
struct sockaddr_in clnt_addr; // 客户端地址结构体
memset(&clnt_addr, 0, sizeof(clnt_addr)); // 清空内容
socklen_t clnt_addr_size = sizeof(clnt_addr); // 计算客户端地址结构体的大小
// 接收客户端发来的请求,创建一个与客户端的套接字连接
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
// 检查是否成功接收客户端请求
if (clnt_sock > 0)
{
pthread_t threadID;
// 创建一个新的线程来处理客户端请求,并检查线程是否创建成功
if (pthread_create(&threadID, NULL, thread_func, &clnt_sock) != 0)
{
perror("创建线程失败!");
exit(-1); // 终止进程,并释放所有资源
}
}
}
}
int main(int argc, char const *argv[])
{
int serv_sock;
if ((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("套接字创建失败!");
return -1;
}
// 设置SO_REUSEADDR来允许端口复用
int reuse = 1;
if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&reuse, sizeof(reuse)) < 0)
{
perror("设置SO_REUSEADDR 1 失败");
return -1;
}
struct sockaddr_in serv_addr; // 服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 清空内容
// 初始化服务器地址结构体
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socklen_t serv_addr_size = sizeof(serv_addr);
// 将套接字绑定到指定的 IP 地址和端口号
if (bind(serv_sock, (struct sockaddr *)&serv_addr, serv_addr_size) == -1)
{
close(serv_sock);
perror("bind() 执行失败!");
return -1;
}
// 将套接字设置为监听模式,等待客户端连接
if (listen(serv_sock, SOMAXCONN) == -1)
{
close(serv_sock);
perror("listen() 执行失败!");
return -1;
}
// 处理客户端连接
handle_connect(serv_sock);
// 程序结束前关闭套接字,释放资源
close(serv_sock);
return 0;
}
编译运行程序后,启动浏览器,在浏览器中输入网址:
http://127.0.0.1:1234/home.html
访问home.html成功则会看见这样的页面:
如果访问一个不存在的文件,比如将访问网址写成这样:
http://127.0.0.1:1234/test.txt
则服务器会给客户端浏览器传送error.html网页,客户端浏览器将会看到这样子的界面:
顺带附上服务器终端输出内容的截图:
0130select实现多路复用
测试方法: 程序运行之后,直接输入内容再回车即可,程序会重新将用户输入的内容输出一遍。
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <unistd.h> // 引入unistd库,提供对POSIX操作系统API的访问
#include <sys/time.h> // 引入时间相关的系统调用
#include <sys/select.h> // 引入select()函数的头文件
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节
int main(int argc, char const *argv[])
{
fd_set reads, temps; // 定义两个文件描述符集合
FD_ZERO(&reads); // 清空文件描述符集合reads
FD_SET(0, &reads); // 将标准输入(文件描述符为0)添加到文件描述符集合reads中
while (1)
{
temps = reads; // 每次循环都进行重置,将文件描述符集合reads复制到temps中
struct timeval timeout; // 定义timeval结构体用于select函数的超时设置
timeout.tv_sec = 1; // 设置超时时间为1秒
timeout.tv_usec = 0; // 设置超时时间为0微秒
int result = select(1, &temps, 0, 0, &timeout); // 调用select函数,检测文件描述符集合temps是否准备好读操作
if (result == -1) // 如果select函数执行出错
{
perror("select() 执行错误!");
return 0;
}
else if (result == 0) // 如果select函数超时
{
continue;
}
else // 如果select函数检测到文件描述符准备好读操作
{
if (FD_ISSET(0, &temps)) // 如果标准输入(文件描述符为0)在temps文件描述符集合中
{
char buf[BUF_SIZE] = {0}; // 定义一个大小为BUF_SIZE的字符数组,并初始化为0
int str_len = read(0, buf, BUF_SIZE); // 从标准输入读取数据到buf数组中,最多读取BUF_SIZE个字节
buf[str_len] = 0; // 在读取到的字符串末尾添加0,表示字符串结束
printf("%s\n", buf); // 输出读取到的字符串
}
}
}
return 0;
}
运行结果:
0141select多路复用技术实现回声客户端_服务端
相关代码:
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 提供了多种通用工具函数
#include <string.h> // 字符串操作函数库
#include <sys/time.h> // 提供了时间相关的函数和数据结构
#include <sys/socket.h> // 提供套接字相关的函数,如socket()、bind()
#include <netinet/in.h> // 定义了 sockaddr_in 结构体,用于IPv4套接字地址
#include <sys/select.h> // 提供了select()函数,用于IO多路复用
#include <unistd.h> // 提供了基本的系统调用函数
#include <arpa/inet.h> // 提供了IP地址转换函数,如inet_addr()、htons()
#define BUF_SIZE 1024 // 定义缓冲区大小
int main(int argc, char const *argv[])
{
int serv_sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建一个TCP套接字
struct sockaddr_in serv_addr; // 定义一个IPv4套接字地址结构
memset(&serv_addr, 0, sizeof(serv_addr)); // 将结构体清零
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置IP地址为任意地址
serv_addr.sin_port = htons(1234); // 设置端口号为1234
// 绑定套接字到地址和端口
if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
printf("bind() 函数执行出错,服务器绑定信息失败!\n");
close(serv_sock);
return 0;
}
// 开始监听,设置最大连接数为20
if (listen(serv_sock, 20) == -1)
{
printf("listen 函数执行出错,服务器侦听连接失败!\n");
close(serv_sock);
return 0;
}
int max_fd = serv_sock; // 设置最大文件描述符为服务器套接字
fd_set reads, back_reads; // 创建一个文件描述符集合
FD_ZERO(&reads); // 清空文件描述符集合
FD_SET(serv_sock, &reads); // 将服务器套接字添加到集合中
int break_flag = 1; // 退出死循环标志
while (break_flag == 1) // 循环处理客户端连接和消息
{
back_reads = reads; // 复制文件描述符集合,用于select函数
struct timeval timeout; // 设置select函数的超时时间
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int fdNum = select(max_fd + 1, &back_reads, 0, 0, &timeout); // 使用select等待IO事件
if (fdNum == -1)
{
printf("%d\n", __LINE__);
printf("select 函数执行错误!\n");
break;
}
else if (fdNum == 0) // 如果select超时,继续下一次循环
{
continue;
}
else
{
// 遍历文件描述符集合
for (int i = 0; i < max_fd + 1; i++)
{
if (FD_ISSET(i, &back_reads)) // 判断是否有文件描述符在活跃
{
if (i == serv_sock) // 如果是服务器套接字,则处理新的连接
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受新的连接
FD_SET(clnt_sock, &reads); // 将新的客户端套接字添加到集合中
max_fd = max_fd > clnt_sock ? max_fd : clnt_sock; // 更新最大文件描述符
printf("客户端连接成功,socket为:%d\n", clnt_sock);
}
else // 如果是客户端套接字,则处理客户端消息
{
char buf[BUF_SIZE] = {0};
int str_len = read(i, buf, BUF_SIZE - 1); // 从客户端读取消息
printf("客户端发来的数据:%s", buf);
// 如果客户端关闭连接
if (str_len == 0)
{
FD_CLR(i, &reads); // 从集合中移除客户端套接字
close(i); // 关闭客户端套接字
printf("客户端已关闭链接。\n");
}
else
{
write(i, buf, str_len); // 将收到的消息写回客户端
if (strcmp(buf, "exit\n") == 0)
{
printf("检测到退出指令,正在退出中……\n");
break_flag = 0;
}
}
}
}
}
}
}
close(serv_sock); // 关闭服务器的socket
printf("服务端退出程序完毕。\n");
return 0;
}
0142select多路复用技术实现回声客户端_客户端
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库
#define BUF_SIZE 1024 // 定义缓冲区大小
int main(int argc, char const *argv[])
{
// 创建客户端套接字
int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr; // 定义服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址为本地回环地址
serv_addr.sin_port = htons(1234); // 设置服务器端口号为1234
// 连接服务器
connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (1)
{
// 提示用户输入信息
fputs("请输入信息,输入“Q”退出:", stdout);
char message[BUF_SIZE] = {0}; // 初始化消息缓冲区
fgets(message, BUF_SIZE, stdin); // 从标准输入读取一行数据到消息缓冲区
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) // 判断用户是否输入"Q"或"q"
break; // 如果是,跳出循环
write(clnt_sock, message, strlen(message)); // 向服务器发送消息
int result = read(clnt_sock, message, BUF_SIZE - 1); // 从服务器接收消息
message[result] = 0; // 设置消息结束符
printf("来自服务器的信息为:%s\n", message); // 打印接收到的消息
}
close(clnt_sock); // 关闭客户端套接字
return 0; // 程序结束
}
运行结果:
0151poll实现IO多路复用_服务端
相关代码:
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h> // memset()
#include <sys/socket.h> // socket()、bind()
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h> // inet_addr()、htons()
#include <unistd.h> // close()、read()、write()
#include <poll.h> // poll()
#define MAX 100 // 定义pollfd数组的最大长度为100
#define BUF_SIZE 1024 // 定义缓冲区大小为1024字节
int pfd_count = 0; // 记录当前pollfd数组中已使用的元素数量
/**
* @brief 初始化pollfd数组中所有元素。
* @param pfd[] 要初始化的pollfd结构体数组。
* @note 该函数将所有文件描述符设置为-1,表示没有设置文件描述符,并清除每个元素的事件和返回事件。
*/
void pfd_init(struct pollfd pfd[MAX])
{
for (int i = 0; i < MAX; i++)
{
pfd[i].fd = -1; // 将文件描述符设置为-1(表示没有设置文件描述符)。
pfd[i].events = 0; // 清除事件。
pfd[i].revents = 0; // 清除返回事件。
}
}
/**
* @brief 向pollfd数组中添加一个带有指定事件的文件描述符。
* @param pfd[] pollfd结构体数组。
* @param fd 要添加到数组的文件描述符。
* @param events 要监视的事件的按位或。
* @note 如果数组已满,该函数将打印错误消息并退出程序。
*/
void pfd_add(struct pollfd pfd[MAX], int fd, short events)
{
if (pfd_count == MAX)
{
printf("pollfd数组已满,pfd_add 函数执行错误!\n"); // 数组满时的错误消息。
exit(-1); // 以失败状态退出程序。
}
pfd[pfd_count].fd = fd; // 分配文件描述符。
pfd[pfd_count].events = events; // 分配要监视的事件。
pfd[pfd_count].revents = 0; // 清除该pollfd的返回事件。
++pfd_count; // 增加活动文件描述符的数量。
}
/**
* @brief 从pollfd数组中删除一个文件描述符。
* @param pfd[] pollfd结构体数组。
* @param fd 要从数组中删除的文件描述符。
* @note 该函数在数组中搜索文件描述符,如果找到,则将其字段重置为默认值。
*/
void pfd_del(struct pollfd pfd[MAX], int fd)
{
for (int i = 0; i < MAX; i++)
{
if (pfd[i].fd == fd)
{
pfd[i].fd = -1; // 重置文件描述符为-1。
pfd[i].events = 0; // 清除事件。
pfd[i].revents = 0; // 清除返回事件。
break;
}
}
}
int main(int argc, char const *argv[])
{
int serv_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建一个TCP套接字
struct sockaddr_in serv_addr; // 定义服务器地址结构体
socklen_t serv_addr_len = sizeof(serv_addr); // 获取服务器地址结构体的大小
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体为0
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_port = htons(1234); // 设置端口号为1234,htons用于网络字节序转换
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 设置IP地址为本地任意地址
// 绑定套接字到本地地址
if (bind(serv_sock, (struct sockaddr *)&serv_addr, serv_addr_len))
{
perror("bind函数执行失败,绑定服务器信息失败!"); // 如果绑定失败,打印错误信息
close(serv_sock); // 关闭套接字
return -1; // 返回-1表示绑定失败
}
// 将套接字设置为监听模式,并设置最大连接数为20
if (listen(serv_sock, 20))
{
perror("listen 侦听失败!"); // 如果监听失败,打印错误信息
close(serv_sock); // 关闭套接字
return -1; // 返回-1表示监听失败
}
struct pollfd pfd[MAX]; // 定义pollfd结构体数组,用于IO多路复用
pfd_init(pfd); // 初始化pollfd数组
pfd_add(pfd, serv_sock, POLLIN); // 将服务器套接字添加到pollfd数组中,并设置监听读事件
// 进入一个无限循环,用于持续监听和处理网络事件
while (1)
{
// 声明一个整型变量result,用于存储poll函数的返回值
int result;
// 调用poll函数等待I/O事件,如果返回-1表示出错
if ((result = poll(pfd, MAX, 5000)) == -1)
{
printf("poll() 函数执行错误!所在行:%d\n", __LINE__); // 打印错误信息,并输出错误发生的行号
exit(1); // 遇到错误时退出程序
}
// 如果poll函数返回0,表示等待超时
if (result == 0)
{
printf("超时,重新监听。\n"); // 打印超时信息
continue; // 继续下一次循环,重新监听
}
// 遍历pollfd数组,处理每个文件描述符的事件
for (int i = 0; i < MAX; i++)
{
// 如果文件描述符为-1,表示无效,跳过
if (pfd[i].fd == -1)
{
continue; // 继续下一个文件描述符
}
// 如果revents字段包含POLLIN,表示有数据可读
if (pfd[i].revents & POLLIN)
{
// 如果是服务器socket有数据可读,表示有新的连接
if (pfd[i].fd == serv_sock)
{
struct sockaddr_in clnt_addr; // 声明一个sockaddr_in结构体,用于存储客户端地址信息
socklen_t clnt_addr_size = sizeof(clnt_addr); // 客户端地址结构的大小
int clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size); // 接受新的连接,并获取客户端socket
pfd_add(pfd, clnt_sock, POLLIN); // 将新的客户端socket添加到pollfd数组中,以便监听其事件
printf("已连接的客户端socket为:%d\n", clnt_sock); // 打印已连接的客户端socket编号
}
// 如果不是服务器socket,表示是客户端socket有数据可读
else
{
char buf[BUF_SIZE] = {0}; // 声明一个字符数组用于存储读到的数据,并初始化为0
int str_len = read(pfd[i].fd, buf, BUF_SIZE - 1); // 从客户端socket读取数据
printf("客户端发来的数据:%s\n", buf); // 打印客户端发来的数据
// 如果读取的字节数为0,表示客户端断开了连接
if (str_len == 0)
{
printf("客户端socket %d 失去连接\n", pfd[i].fd); // 打印失去连接的信息
close(pfd[i].fd); // 关闭客户端socket
pfd_del(pfd, pfd[i].fd); // 从pollfd数组中删除对应的socket
}
// 如果读取到了数据
else
{
write(pfd[i].fd, buf, str_len); // 将数据写回客户端,实现回声服务
}
}
}
}
}
// 关闭服务端的socket
close(serv_sock);
return 0;
}
0152poll实现IO多路复用_客户端
相关代码:
#include <stdio.h> // 引入标准输入输出库
#include <string.h> // 引入字符串操作库
#include <stdlib.h> // 引入标准库
#include <unistd.h> // 引入Unix标准函数库
#include <arpa/inet.h> // 引入IP地址转换库
#include <sys/socket.h> // 引入套接字库
#define BUF_SIZE 1024 // 定义缓冲区大小
int main(int argc, char const *argv[])
{
// 创建客户端套接字
int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr; // 定义服务器地址结构体
memset(&serv_addr, 0, sizeof(serv_addr)); // 初始化服务器地址结构体
serv_addr.sin_family = AF_INET; // 设置地址家族为IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 设置服务器IP地址为本地回环地址
serv_addr.sin_port = htons(1234); // 设置服务器端口号为1234
// 连接服务器
connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while (1)
{
// 提示用户输入信息
fputs("请输入信息,输入“Q”退出:", stdout);
char message[BUF_SIZE] = {0}; // 初始化消息缓冲区
fgets(message, BUF_SIZE, stdin); // 从标准输入读取一行数据到消息缓冲区
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) // 判断用户是否输入"Q"或"q"
break; // 如果是,跳出循环
write(clnt_sock, message, strlen(message)); // 向服务器发送消息
int result = read(clnt_sock, message, BUF_SIZE - 1); // 从服务器接收消息
message[result] = 0; // 设置消息结束符
printf("来自服务器的信息为:%s\n", message); // 打印接收到的消息
}
close(clnt_sock); // 关闭客户端套接字
return 0; // 程序结束
}
运行结果: