一、TCP概述
1、TCP特点
- 面向连接的流式协议; 可靠、出错重传,且每收到一个数据都要给出相应的确认 (ACK)
- 通讯之前 必须先建立连接
- 服务器被动连接,客户端主动连接
- TCP 不支持传播0字节报文
2、TCP流程
3、TCP客户端流程
1、创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket error:");
return -1;
}
printf("sockfd = %d\n",sockfd);
2、connect:建立连接
客户端调用 connect函数后 就会触发 三次握手
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:
主动跟服务器建立连接
参数:
sockfd: 需要建立连接的套接字
addr: 服务器地址 struct sockaddr_in dest_addr = {0}; IP port family 赋值
addrlen: 服务器地址结构体长度 sizeof(dest_addr)
返回值:
成功 0
失败 -1 errno
注意:
1、connect建立连接之后不会创建新的套接字
2、只有连接成功之后 才能 传输 TCP数据!!!!
3、发送:send/write
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:
用于发送数据
参数:
sockfd: 已建立连接的套接字
buf: 发送数据的地址
nbytes: 发送缓数据的大小(以字节为单位)
flags: 套接字标志(常为 0)
返回值:
成功发送的字节数
注意:
不能用 TCP 协议发送 0 长度的数据
4、接收:recv/read
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:
用于接收网络数据
参数:
sockfd:套接字
buf: 接收网络数据的缓冲区的地址
nbytes: 接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为 0)
返回值:
成功 实际接收到字节数
5、关闭套接字
close(sockfd);
6、客户端demo
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
int main(int argc, char const *argv[])
{
// 1、创建流式套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 2、建立连接
// 确定服务器地址
struct sockaddr_in saddr = {0};
saddr.sin_family = AF_INET;
saddr.sin_port = htons(8080);
inet_pton(AF_INET, "10.7.164.45", &saddr.sin_addr.s_addr);
int ret = connect(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret != 0)
{
perror("connet error:");
close(sfd);
return -1;
}
// 连接过后的具体操作
send(sfd, "hello TCP server!", 18, 0);
while (1)
{
char r_buf[512] = {0};
int r_len = recv(sfd, r_buf, sizeof(r_buf), 0);
if (r_len > 0)
{
printf("recv:%s\n", r_buf);
}
if (strcmp(r_buf, "quit") == 0)
{
printf("断开连接!\n");
break;
}
}
close(sfd);
return 0;
}
4、TCP服务器流程
1、socket:创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket error:");
return -1;
}
printf("sockfd = %d\n",sockfd);
2、bind:绑定服务器自己的IP与端口
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *address,
socklen_t address_len);
给 套接字sockfd 绑定固定的IP和端口
3、listen:监听客户端请求
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
将套接字由主动修改为被动,等待客户端的连接请求
使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
参数:
sockfd: socket监听套接字
backlog:连接队列的长度(客户端的个数)
返回值:
成功:返回0
失败:-1
4、accept:提取客户端消息
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址
返回值:
成功 : 已连接套接字 ---> 新建套接字
失败-1
5、send/recv:发送和接收
ssize_t send(int socket, const void *buffer, size_t length, int flags);
ssize_t recv(int socket, void *buffer, size_t length, int flags);
6、close:关闭套接字
1、关闭 已连接的套接字 ----> accept 返回的新套接字 主要用于 已经和客户端建立连接的套接字 ---- 可以和客户端通信
不会影响 监听套接字 和 已经连接的其他套接字
2、关闭监听套接字 ----> socket 返回的套接字 ----> 主要用于监听的套接字
已经连接的套接字 不受影响, 只是不能再获取新的客户端请求
7、服务器demo
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#define PORT 6585
int main(int argc, char const *argv[])
{
// 1、创建监听套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 10);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len;
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
// 获取客户端的IP和端口
char cli_ip[16] = {0};
unsigned short cli_port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cli_ip, 16);
printf("客户端:%s : %d 连接成功!\n", cli_ip, cli_port);
while (1)
{
// 通过新的描述符 和客户端进行通信
// recv
char r_buf[512] = {0};
int r_len = recv(new_fd, r_buf, 512, 0);
printf("recv: %s\n", r_buf);
if (strcmp(r_buf, "quit") == 0)
{
send(new_fd, "quit", 4, 0);
break;
}
// send
send(new_fd, "OK!", 3, 0);
}
close(new_fd);
}
close(sfd);
return 0;
}
二、三次握手和四次挥手
1、TCP的报文
序列号: seq 当前报文的序列号!!!!
确认号: ack 期望收到对方下一个报文段的序列号!!
ACK : 确认收到
SYN: SYN 设置1 表示这是一个连接请求的报文
FIN: FIN 设置1 表示 这是一个关闭连接请求的报文!!! 并要求释放连接!!
2、三次握手和四次挥手的过程
1、三次握手建立
一、准备工作
才开始 客户端和服务器都处于 closed,主动打开链接的 是 客户端 ,被动连接的是 服务器
TCP服务器进程 会创建一个TCB 传输控制块, 时刻准备 接收客户端的连接请求 ----> 服务器 就进入 listen(监听)状态
二、第一次握手
客户端进程也会会创建TCB ----> 向服务器发送连接请求报文! ------> SYN = 1 并且设置/选择一个初始序列号 x ------> seq = x
客户端进入 SYN-SEND(同步已发送状态) -------> TCP 有规定 SYN 为1 不能携带数据段!!! 但是要消耗一个序列号!!!
三、二次握手
服务器收到客户端的请求报文,如果同意连接,则发出确认连接报文 ----> ACK = 1 SYN=1 确认号 ack = x+1 -----> 同时服务器也要初始化一个自己的序列号 seq = y
服务器进入SYN-RCVD(同步收到)状态, -----> 这个报文也不能携带数据段!!! 但是也要消耗一个序列号
ACK = 1表示确定 有效 0 表示报文中不包含确认信息
四、三次握手
客户端收到 服务器的ACK 确认!!! -----> 向服务器 给出自己的确认!!! ---> ACK = 1, ack = y+1 自己序列号 seq = x+1
这是 TCP连接 就建立了!!! 客户端就进入一个 ESTABLISHED (已建立连接) -----> ACK报文可以携带数据段!! 如果不带数据段 就不消耗序列号!!!!
当服务器收到 客户端的确认后 进入 已建立连接状态 -----> 这样双方 就可以开始通讯
2、四次挥手
一、一次挥手
客户端进程 主动发出 释放连接的报文 , 并且停止 数据发送,释放数据报文 首部 -----> FIN=1(FIN 表示 关闭连接 SYN 建立连接)
客户端 它的序列号 为 u, ------》 FIN = 1 seq = u ---------> FIN 就算不携带数据 也会消耗一个序列号
客户端处于 FIN-WAIT-1 ( 释放等待态1)
二、二次挥手
服务器收到 客户端 释放连接报文, 要告诉客户端 我收到你的释放连接请求了, ACK = 1 ack = u+1 ------> 自己序列号 seq = v
这个时候并没有断开!!!! 只是表示 服务器 直到 客户端 发送的 释放连接的请求
这个时候服务器 处于 CLOSE-WAIT(关闭等待态)
服务器 如果有 没有发完的数据 ,会继续发送数据 直到发送完成 客户端 仍然会接受数据!!
如果服务器 回复的ACK 确认状态 被客户端收到 ----> 客户端会进入 FIN-WAIT-2(释放等待态2);
三、三次挥手
服务的数据如果 发完了, 服务器 也会发出 释放连接的报文 -----> ACK = 1, ack = u+1 ----> 自己序列号 假如中间又发送的有数据 当前序列号为 w seq = w
-----> FIN = 1 ACK = 1 ack = u+1 seq = w
服务器状态变成 LAST-ACK(最后确认状态)
四、四次挥手
客户端收到 服务器的 确认退出 报文之后 回复收到 -----> ACK = 1 seq = u+1 ack=w+1
此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器 收到 客户端的确认 之后 进入 CLOSED 状态
1.发送过去的ACK 最大存活时间(MSL) + 来向的FIN 最长存活时间 MSL
2.第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
3. 第二,等待2MSL时间,客户端就可以放心地释放TCP占用的资源、端口号。如果不等,释放的端口可能会重连刚断开的服务器端口,这样依然存活在网络里的老的TCP报文可能与新TCP连接报文冲突,造成数据冲突,为避免此种情况,需要耐心等待网络老的TCP连接的活跃报文全部死翘翘,2MSL时间可以满足这个需求(尽管非常保守)!
3、TCP网络状态
CLOSED:表示没有活跃的连接,也没有等待任何连接的请求。
LISTEN:服务器在等待来自客户端的连接请求。
SYN_SENT:应用程序(通常是客户端)已经开始了一个连接请求,并且已经发送了一个SYN(同步)数据包到服务器,等待服务器的响应。
SYN_RECEIVED:服务器已经收到了来自客户端的SYN数据包,并且已经回应了自己的SYN和ACK(确认)数据包。
ESTABLISHED:双方都已经收到了对方的SYN数据包,此时连接已经建立,数据可以在两个方向上传输。
FIN_WAIT_1:应用程序告诉TCP它已经完成了数据传输,TCP正在等待所有剩余的数据被传输完。
FIN_WAIT_2:客户端已经收到了服务器的ACK数据包,正在等待服务器关闭它的连接。
CLOSE_WAIT:服务器已经收到了客户端的FIN数据包,并且已经回应了ACK数据包,正在等待服务器应用程序关闭连接。
LAST_ACK:服务器正在等待客户端的最后一个ACK数据包。
CLOSING:双方都试图同时关闭连接,此时正在等待对方的ACK数据包。
TIME_WAIT:等待足够的时间以确保对方已经收到了最后一个ACK数据包
4、案例:如果TCP服务器先关闭
1.当服务器认为数据已经发送完成,它发送一个FIN数据包给客户端。这时,服务器进入FIN_WAIT_1状态。
2.客户端收到服务器的FIN数据包后,它回复一个ACK数据包,并进入CLOSE_WAIT状态。当服务器收到这个ACK数据包后,服务器进入FIN_WAIT_2状态。
3.如果客户端也认为数据传输已经完成,它会发送一个FIN数据包给服务器。然后,客户端进入LAST_ACK状态。
4.当服务器收到客户端的FIN数据包后,它回复一个ACK数据包,并进入TIME_WAIT状态。此时,客户端收到这个ACK数据包后,它关闭连接并进入CLOSED状态。
5.服务器在TIME_WAIT状态等待一段时间,确保ACK数据包被客户端正确接收。之后,服务器也关闭连接并进入CLOSED状态。
5、案例:如果TCP同时关闭
1.客户端发送FIN数据包给服务器,并进入FIN_WAIT_1状态。
2.同时,服务器也发送FIN数据包给客户端,并进入FIN_WAIT_1状态。
3.客户端收到服务器的FIN数据包,回复确认,然后进入CLOSING状态。
4.服务器收到客户端的FIN数据包,回复确认,然后也进入CLOSING状态。
5.服务器收到客户端的ACK数据包,转移到TIME_WAIT状态。
6.客户端收到服务器的ACK数据包,也转移到TIME_WAIT状态。
6、TIME_WAIT
保障双方最后的退出 确认报文 都能够收到!!!
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。这恰恰就是**2MSL( Maximum Segment Life)。
(1)确保最后的ACK被正确接收
(2)避免旧的数据包干扰新的连接
(3)让网络中的延迟数据包消失
四、并发服务器
1、多进程实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#include <signal.h>
#include <sys/wait.h>
#define PORT 9988
// 信号自处理回调
void del_pid_func(int signum);
// 连接后的功能函数
void client_func(int new_fd, char ip[], unsigned short port);
int main(int argc, char const *argv[])
{
// 注册一个信号处理函数 --- 子进程自动回收 防止僵尸进程
signal(SIGCHLD, del_pid_func);
// 1、创建监听套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 3);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
else
{
// 获取客户端的IP和端口
char cli_ip[16] = {0};
unsigned short cli_port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, cli_ip, 16);
printf("客户端:%s : %d 连接成功!\n", cli_ip, cli_port);
pid_t pid = fork();
if (pid == 0)
{
close(sfd);
// 子进程处理函数
client_func(new_fd, cli_ip, cli_port);
close(new_fd);
exit(0);
}
close(new_fd);
}
}
close(sfd);
return 0;
}
void client_func(int new_fd, char ip[], unsigned short port)
{
while (1)
{
// 通过新的描述符 和客户端进行通信
// recv
char r_buf[512] = {0};
int r_len = recv(new_fd, r_buf, 512, 0);
if (r_len == 0)
{
break;
}
printf("recv: %s %hu:%s\n", ip, port, r_buf);
if (strcmp(r_buf, "quit") == 0)
{
send(new_fd, "quit", 4, 0);
break;
}
// send
send(new_fd, "OK!", 3, 0);
}
}
void del_pid_func(int signum)
{
if (signum == SIGCHLD)
{
// 自动回收子进程资源
pid_t pid = waitpid(-1, NULL, WNOHANG);
if (pid > 0)
{
printf("自动回收 子进程 %d\n", pid);
}
}
}
2、多线程实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define PORT 9988
int sfd = 0;
typedef struct pthread_msg
{
char ip[16];
unsigned short port;
int fd;
} PMSG;
// 信号自处理回调
void close_func(int signum);
// 连接后的功能函数
void *client_func(void *arg);
int main(int argc, char const *argv[])
{
// 注册一个信号处理函数
signal(SIGINT, close_func);
// 1、创建监听套接字
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 3);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
else
{
PMSG data = {0};
data.port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, data.ip, 16);
printf("客户端:%s : %d 连接成功!\n", data.ip, data.port);
data.fd = new_fd;
pthread_t lwp;
pthread_create(&lwp, NULL, client_func, (void *)&data);
pthread_detach(lwp); // 线程分离 系统回收
}
}
close(sfd);
return 0;
}
void *client_func(void *arg)
{
int fd = ((PMSG *)arg)->fd;
while (1)
{
// 通过新的描述符 和客户端进行通信
// recv
char r_buf[512] = {0};
int r_len = recv(fd, r_buf, 512, 0);
if (r_len == 0)
{
break;
}
printf("recv: %s %hu:%s\n", ((PMSG *)arg)->ip, ((PMSG *)arg)->port, r_buf);
if (strcmp(r_buf, "quit") == 0)
{
send(fd, "quit", 4, 0);
break;
}
// send
send(fd, "OK!", 3, 0);
}
close(fd);
pthread_exit(NULL);
return NULL;
}
void close_func(int signum)
{
if (signum == SIGINT)
{
close(sfd);
printf("监听套接字关闭!\n");
exit(0);
}
}
每当有客户端连接服务器成功 ---> 就会新建线程 data 的 值 就被更改
每当有客户端连接服务器成功 ---> 就会新建线程 data 的 值 就被更改
新建变量 将ip 和port 都获取出来 ------> 在线程内部存储使用 不能直接使用指针!!!
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define PORT 9988
int sfd = 0;
typedef struct pthread_msg
{
char ip[16];
unsigned short port;
int fd;
} PMSG;
// 信号自处理回调
void close_func(int signum);
// 连接后的功能函数
void *client_func(void *arg);
int main(int argc, char const *argv[])
{
// 注册一个信号处理函数
signal(SIGINT, close_func);
// 1、创建监听套接字
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 3);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
else
{
PMSG data = {0};
data.port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, data.ip, 16);
printf("客户端:%s : %d 连接成功!\n", data.ip, data.port);
data.fd = new_fd;
pthread_t lwp;
pthread_create(&lwp, NULL, client_func, (void *)&data);
pthread_detach(lwp); // 线程分离 系统回收
}
}
close(sfd);
return 0;
}
void *client_func(void *arg)
{
int fd = ((PMSG *)arg)->fd;
char ip[16] = {0};
strcpy(ip, ((PMSG *)arg)->ip);
unsigned short port = ((PMSG *)arg)->port;
while (1)
{
// 通过新的描述符 和客户端进行通信
// recv
char r_buf[512] = {0};
int r_len = recv(fd, r_buf, 512, 0);
if (r_len == 0)
{
break;
}
printf("recv: %s %hu:%s\n", ip, port, r_buf);
if (strcmp(r_buf, "quit") == 0)
{
send(fd, "quit", 4, 0);
break;
}
// send
send(fd, "OK!", 3, 0);
}
close(fd);
pthread_exit(NULL);
return NULL;
}
void close_func(int signum)
{
if (signum == SIGINT)
{
close(sfd);
printf("监听套接字关闭!\n");
exit(0);
}
}
五、WEB服务器
1、http:超文本传输协议 应用层
特点: 浏览器 和万维网服务器之间的通信规则 通过因特网传输万维网的文档或数据的协议
支持 C/S 架构
简单快速: 客户端向服务器发送请求的时候 只需要传输方法和路径 常用方法: GET POST
无状态: 如果后续的处理需要用到前面的信息,它重传 -----> 导致 每次连接的时候数据量都会增大!!!
web服务器一般都是并发服务器
2、客户端请求
GET /index.html HTTP/1.1
3、web服务器流程
创建套接字 socket
端口复用 ---> 让监听套接字 支持 端口复用 重复使用 !!!
int opt = 1; ----> 非0 表示启用
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
绑定服务器地址 bind
监听套接字 linsten
循环等待连接 accept
接收数据
解析报文 ----> 获取 客户端想要的文件名 ---->判断本地是否有改文件
有: 组包 200 OK 不断读取文件内容发送给客户端
没有: 发送404 结束当前任务
close
4、多线程WEB服务器
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define PORT 9988
int sfd = 0;
typedef struct pthread_msg
{
char ip[16];
unsigned short port;
int fd;
} PMSG;
char head[] = {
"HTTP1.1 200 OK\r\n" // 使用HTTP/1.1版本,状态码200表示请求成功,OK为状态描述
"Content-Type: text/html\r\n" // 响应的内容类型为HTML
"\r\n" // 空行,分隔头部和响应体
};
char err[] = {
"HTTP1.1 404 Not Found\r\n" // 使用HTTP/1.1版本,状态码200表示请求成功,OK为状态描述
"Content-Type: text/html\r\n" // 响应的内容类型为HTML
"\r\n" // 空行,分隔头部和响应体
"<HTML><body>ERROR !! 文件不存在!</body></HTML>"};
// 信号自处理回调
void close_func(int signum);
// 连接后的功能函数
void *client_func(void *arg);
int main(int argc, char const *argv[])
{
// 注册一个信号处理函数
signal(SIGINT, close_func);
// 1、创建监听套接字
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 设置端口复用
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 3);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
else
{
PMSG data = {0};
data.port = ntohs(cli_addr.sin_port);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, data.ip, 16);
printf("客户端:%s : %d 连接成功!\n", data.ip, data.port);
data.fd = new_fd;
pthread_t lwp;
pthread_create(&lwp, NULL, client_func, (void *)&data);
pthread_detach(lwp); // 线程分离 系统回收
}
}
close(sfd);
return 0;
}
void *client_func(void *arg)
{
int fd = ((PMSG *)arg)->fd;
char ip[16] = {0};
strcpy(ip, ((PMSG *)arg)->ip);
unsigned short port = ((PMSG *)arg)->port;
// 获取请求
char r_buf[512] = {0};
int r_len = recv(fd, r_buf, sizeof(r_buf), 0);
if (r_len <= 0)
{
close(fd);
pthread_exit(NULL);
return NULL;
}
// 解析请求
if (strstr(r_buf, "GET /index.html") != NULL)
{
int f_fd = open("./index.html", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else if (strstr(r_buf, "GET /1.html") != NULL)
{
int f_fd = open("./1.html", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else
{
// 没有 对应的 超文本 显示 404
send(fd, err, strlen(err), 0);
}
close(fd);
pthread_exit(NULL);
return NULL;
}
void close_func(int signum)
{
if (signum == SIGINT)
{
close(sfd);
printf("监听套接字关闭!\n");
exit(0);
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
Data1:<input type="text" id="data1">
<br>
Data2:<input type="text" id="data2">
<br>
Ret:<input type="text" id="ret">
<br>
<input type="button" value="加法" onclick="my_calc(1);">
<input type="button" value="减法" onclick="my_calc(0);">
<br>
hello<br> 你好<br>
</body>
</html>
1.html
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
hello<br>
你好<br>
成都物联网2401班<br>
</body>
</html>
六、线程池
1、线程池的概念
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。
超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
2、线程的逻辑
创建一个任务队列,多个线程等待从队列中取出任务执行,任务队列为空的时候,线程会等待新任务可用,或者超时,当线程池销毁的时候,所有线程都会结束。
3、线程池的实现(了解)
threadpool.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
//gcc threadpool.c -o threadpool -lpthread -lrt
// 定义互斥锁和条件变量的结构体
typedef struct condition {
pthread_mutex_t pmutex; // 互斥锁
pthread_cond_t pcond; // 条件变量
} condition_t;
// 定义任务结构体
typedef struct task {
void *(*run)(void *arg); // 任务函数指针
void *arg; // 任务函数参数
struct task *next; // 下一个任务
} task_t;
// 定义线程池结构体
typedef struct threadpool {
condition_t ready; // 同步条件变量
task_t *first; // 任务列表头部
task_t *last; // 任务列表尾部
int counter; // 线程计数器
int idle; // 空闲线程数量
int max_threads; // 最大线程数
int quit; // 销毁标志
} threadpool_t;
// 锁定互斥锁
int condition_lock(condition_t *cond)
{
return pthread_mutex_lock(&cond -> pmutex);
}
// 解锁互斥锁
int condition_unlock(condition_t *cond)
{
return pthread_mutex_unlock(&cond -> pmutex);
}
// 阻塞等待条件变量
int condition_wait(condition_t *cond)//阻塞
{
return pthread_cond_wait(&cond -> pcond,&cond -> pmutex);
}
// 指定时间阻塞等待条件变量
int condition_timewait(condition_t *cond,const struct timespec *abstime)//阻塞指定时间
{
return pthread_cond_timedwait(&cond->pcond,&cond->pmutex,abstime);
}
// 唤醒一个等待条件变量的线程
int condition_signal(condition_t *cond)//唤醒
{
return pthread_cond_signal(&cond->pcond);
}
// 唤醒所有等待条件变量的线程
int condition_broadcast(condition_t *cond)//唤醒
{
return pthread_cond_broadcast(&cond -> pcond);
}
// 销毁条件变量和互斥锁
int condition_destory(condition_t *cond)//销毁条件变量和互斥锁
{
int status;
if((status = pthread_mutex_destroy(&cond -> pmutex)))
return status;
if((status = pthread_cond_destroy(&cond -> pcond)))
return status;
return 0;
}
void *thread_routine(void *arg)
{
struct timespec abstime; // 用于计算等待条件变量的超时时间
int timeout; // 超时标志,如果为1表示线程在等待时超时
threadpool_t *pool = (threadpool_t *)arg; // 将传入的参数转换为线程池结构体指针
while(1) // 使线程持续执行,除非收到退出信号
{
timeout = 0; // 初始设置超时标志为0
condition_lock(&pool->ready); // 锁定线程池的互斥锁,开始访问线程池状态
pool->idle++; // 增加空闲线程计数,因为线程现在没有执行任何任务
// 当任务列表为空,并且线程池没有设置为退出时,线程会在条件变量上等待
while(pool->first == NULL && !pool->quit)
{
clock_gettime(CLOCK_REALTIME, &abstime); // 获取当前时间
abstime.tv_sec += 2; // 设置超时时间为2秒
// 让线程在条件变量上等待,直到被唤醒或超过2秒
int status = condition_timewait(&pool->ready, &abstime);
// 如果线程因为超时而被唤醒,设置超时标志为1并退出等待循环
if(status == ETIMEDOUT)
{
timeout = 1;
break;
}
}
pool->idle--; // 减少空闲线程计数,因为线程可能即将开始执行任务
// 如果任务列表不为空,线程从任务列表中取出一个任务并执行它
if(pool->first != NULL)
{
task_t *t = pool->first; // 获取任务列表的第一个任务
pool->first = t->next; // 移动任务列表的头指针
// 解锁互斥锁,以允许其他线程访问线程池
condition_unlock(&pool->ready);
t->run(t->arg); // 执行任务
free(t); // 释放已完成的任务内存
condition_lock(&pool->ready); // 再次锁定互斥锁,以继续访问线程池状态
}
// 如果设置了线程池的退出标志,并且任务列表为空,线程将结束
if(pool->quit && pool->first == NULL)
{
pool->counter--; // 减少线程计数器
// 如果这是最后一个线程,通知其他可能等待线程池销毁的线程
if(pool->counter == 0)
{
condition_signal(&pool->ready);
}
condition_unlock(&pool->ready); // 解锁互斥锁
break; // 退出线程
}
// 如果线程在等待任务时超时,并且任务列表为空,线程将结束
if(timeout && pool->first == NULL)
{
pool->counter--; // 减少线程计数器
condition_unlock(&pool->ready); // 解锁互斥锁
break; // 退出线程
}
condition_unlock(&pool->ready); // 如果线程不退出,解锁互斥锁并继续下一次循环
}
return NULL; // 线程结束
}
// 销毁线程池函数
void threadpool_destory(threadpool_t *pool)
{
// 如果线程池已经被设置为退出,直接返回
if(pool->quit)
{
return;
}
// 锁定线程池的互斥锁,开始修改线程池状态
condition_lock(&pool->ready);
// 设置线程池的退出标志为1,表示线程池即将被销毁
pool->quit = 1;
// 如果还有线程在运行
if(pool->counter > 0)
{
// 如果有空闲线程,使用条件变量的广播函数唤醒所有等待的线程
if(pool->idle > 0)
condition_broadcast(&pool->ready);
// 等待所有线程结束。当每个线程结束时,它会减少counter的值。
// 因此,这里循环等待,直到所有线程都结束。
while(pool->counter > 0)
{
condition_wait(&pool->ready);
}
}
// 解锁线程池的互斥锁
condition_unlock(&pool->ready);
// 销毁与线程池关联的条件变量和互斥锁
condition_destory(&pool->ready);
}
//增加任务,run是要处理的任务,arg是传给任务的参数
void threadpool_add_task(threadpool_t *pool, void *(*run)(void *arg), void *arg)
{
// 分配内存并初始化新任务结构体
task_t *newstask = (task_t *)malloc(sizeof(task_t));
newstask->run = run; // 设置任务函数
newstask->arg = arg; // 设置任务函数的参数
newstask->next = NULL; // 设置下一个任务为NULL,因为这是新添加的任务
// 锁定线程池的互斥锁,以便安全地修改任务队列
condition_lock(&pool->ready);
// 如果任务队列为空,新任务成为队列的第一个任务
if(pool->first == NULL)
{
pool->first = newstask;
}
else
{
// 否则,添加新任务到任务队列的末尾
pool->last->next = newstask;
}
pool->last = newstask; // 设置新任务为任务队列的末尾
// 如果有空闲线程,使用条件变量唤醒其中一个
if(pool->idle > 0)
{
condition_signal(&pool->ready);
}
// 如果没有空闲线程,并且当前线程数量小于最大线程数量,创建一个新线程
else if(pool->counter < pool->max_threads)
{
pthread_t tid;
pthread_create(&tid, NULL, thread_routine, pool); // 创建新线程
pool->counter++; // 增加线程计数
}
// 解锁线程池的互斥锁
condition_unlock(&pool->ready);
}
//初始化条件变量和互斥锁
int condition_init(condition_t *cond)
{
int status;
// 初始化互斥锁,成功返回0
if((status = pthread_mutex_init(&cond->pmutex, NULL)))
return status;
// 初始化条件变量,成功返回0
if((status = pthread_cond_init(&cond->pcond, NULL)))
return status;
return 0; // 如果都成功,返回0
}
// 初始化线程池
void threadpool_init(threadpool_t *pool, int threads)
{
// 初始化与线程池相关的条件变量和互斥锁
condition_init(&pool->ready);
pool->first = NULL; // 设置任务队列的头为NULL
pool->last = NULL; // 设置任务队列的尾为NULL
pool->counter = 0; // 初始设置线程计数为0
pool->idle = 0; // 初始设置空闲线程数量为0
pool->max_threads = threads; // 设置线程池的最大线程数量
pool->quit = 0; // 设置线程池的退出标志为0,表示线程池正在运行
}
4、基于线程池的web服务器 (掌握)
1、使用线程池的方法
1、头文件
#include <pthreadpool.h>
2、实例化线程池 ----> 定义一个线程池变量
theadpool_t pool;
3、主函数中初始化 线程池
threadpool_init(&pool,30); // 线程池初始化最大值 允许30个线程
4、添加任务到线程池
threadpool_add_task(&pool,client_func,new_fd);
5、结束后销毁线程池 ---> 收到终止信号
threadpool_destory(&pool);
2、web服务器 线程池实现
gcc web.c threadpool.c -o webserver -lpthread
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // inet_pton
#include <netinet/in.h> // 地址结构体头文件
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
volatile int flag = 1;
#define PORT 9988
int sfd = 0;
char head[] = {
"HTTP1.1 200 OK\r\n" // 使用HTTP/1.1版本,状态码200表示请求成功,OK为状态描述
"Content-Type: text/html\r\n" // 响应的内容类型为HTML
"\r\n" // 空行,分隔头部和响应体
};
char err[] = {
"HTTP1.1 404 Not Found\r\n" // 使用HTTP/1.1版本,状态码200表示请求成功,OK为状态描述
"Content-Type: text/html\r\n" // 响应的内容类型为HTML
"\r\n" // 空行,分隔头部和响应体
"<HTML><body>ERROR !! 文件不存在!</body></HTML>"};
threadpool_t pool;
// 信号自处理回调
void close_func(int signum);
// 连接后的功能函数
void *client_func(void *arg);
int main(int argc, char const *argv[])
{
// 注册一个信号处理函数
signal(SIGINT, close_func);
threadpool_init(&pool);
// 1、创建监听套接字
sfd = socket(AF_INET, SOCK_STREAM, 0);
if (sfd < 0)
{
perror("socket error:");
return -1;
}
// 设置端口复用
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2、绑定服务器地址
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(PORT);
ser_addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(sfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
if (ret != 0)
{
perror("bind error:");
close(sfd);
return -1;
}
// 3、监听 使用listen函数创建连接队列 并且监听客户端的连接
ret = listen(sfd, 3);
if (ret != 0)
{
perror("listen error:");
close(sfd);
return -1;
}
while (1)
{
// 4、使用 accept 阻塞等待客户端的连接 并且为连接新建套接字
// 接受 请求连接的客户端地址
struct sockaddr_in cli_addr = {0};
socklen_t cli_len = sizeof(cli_addr);
int new_fd = accept(sfd, (struct sockaddr *)&cli_addr, &cli_len);
if (new_fd < 0)
{
perror("accept error:");
close(sfd);
return -1;
}
else
{
// pthread_t lwp;
// pthread_create(&lwp, NULL, client_func, (void *)&new_fd);
// pthread_detach(lwp); // 线程分离 系统回收
threadpool_add_task(&pool, client_func, new_fd);
}
}
close(sfd);
return 0;
}
void *client_func(void *arg)
{
int fd = *(int *)arg;
flag = 1;
// 获取请求
char r_buf[512] = {0};
int r_len = recv(fd, r_buf, sizeof(r_buf), 0);
if (r_len <= 0)
{
close(fd);
pthread_exit(NULL);
return NULL;
}
// 解析 请求
if (strstr(r_buf, "GET /index.html") != NULL)
{
int f_fd = open("./index.html", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else if (strstr(r_buf, "GET /1.html") != NULL)
{
int f_fd = open("./1.html", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else if (strstr(r_buf, "GET /2.html") != NULL)
{
int f_fd = open("./2.html", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else if (strstr(r_buf, "1111.jpg") != NULL)
{
int f_fd = open("./1111.jpg", O_RDONLY);
if (f_fd < 0)
{
send(fd, err, strlen(err), 0);
}
// 发送成功响应的头
send(fd, head, strlen(head), 0);
int f_len = 0;
char f_buf[512] = {0};
while ((f_len = read(f_fd, f_buf, sizeof(f_buf))) > 0)
{
send(fd, f_buf, f_len, 0);
}
close(f_fd);
}
else
{
// 没有 对应的 超文本 显示 404
send(fd, err, strlen(err), 0);
}
close(fd);
pthread_exit(NULL);
return NULL;
}
void close_func(int signum)
{
if (signum == SIGINT)
{
close(sfd);
printf("监听套接字关闭!\n");
threadpool_destory(&pool);
exit(0);
}
}