复习三次握手四次挥手
send/recv
recv和send比read和write多一个参数flag,见到flag的时候,就把flag赋值为0.(没讲为啥。。。)
TCP状态转化
三次握手
客户端收到服务器发送的SYN+ACK的时候,客户端的状态就变成了ESTABLISHED,当进入该状态的时候,说明该端可以发送数据进行通信了。
四次挥手
谁先关闭连接谁的状态先发生变化
TCP状态转换图
分析:那些状态能捕捉到,那些捕捉不到(假定客户端发起断开)
捕捉不到
客户端
- SYN_SENT
- FIN_WAIT_1
服务端
- SYN_RCVD
- CLOSE_WAIT
- LAST_ACK
捕捉的到
客户端
- ESTABLISHED
- FIN_WAIT2
- TIME_WAIT
服务端
- ESTABLISHED
2MSL
MSL一般是30秒,2MSL的时间是1min。
主动断开连接的一方要等待2MSL时间,然后结束进程。
等待这段时间是为了对端能正常的销毁进程,为保证对端能收到ack,如果对端没有收到发起断开的一方发送的最后一个ack时,2MSL可以补发ack。为什么会补发ack?因为对端没收到ack就会补发FIN。
半关闭
函数 shutdown
int shutdown(int sockfd, int how);
- sockfd: 要半关闭的一方对应的文件描述符
使用方法
- SHUT_RD - 0 - 读
- SHUT_WR - 1- 写
- SHUT_RDWR -2 - 读写
使用shutdown之后,不管你赋值了多少文件描述符,操作了一个,所有的就都失效了
(复制文件描述符使用dup2函数,new指向old的那一块内存空间,,浅拷贝了目测)
netstat命令
可以捕捉到相应的进程状态。
例子——查看监听8888端口的进程
netstat -apn | grep 8888
端口复用设置
当服务器进程端口连接再重启,会显示以下返回值:
因为进入time wait状态了。
端口复用的用途
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出系统没有释放端口
设置方法:
int opt = 1;
setsockopt(sockfd, SOL_SOCKET,
SO_REUSEADDR,
(const void *)&opt, sizeof(opt));
端口复用只是该函数的一个功能。
man手册截图(这俩函数在unp第七章)
端口复用函数要设置在绑定之前
int main(int argc, const char* argv[])
{
// 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket error");
exit(1);
}
// lfd 和本地的IP port绑定
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET; // 地址族协议 - ipv4
server.sin_port = htons(8888);
server.sin_addr.s_addr = htonl(INADDR_ANY);
//设置端口复用
int flag = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
int ret = bind(lfd, (struct sockaddr*)&server, sizeof(server));
if(ret == -1)
{
perror("bind error");
exit(1);
}
...
}
IO多路转接
IO操作方式
1、阻塞等待
- 好处——不占用cpu时间片
- 缺点——同一时刻只能处理一个操作,效率低。
2、非阻塞,忙轮询
- 优点——提高程序的执行效率
- 缺点——消耗CPU
解决方案——使用IO多路转接技术select/poll/epoll
IO多路转接,我们需要委托内核帮我们做一些事情、委托内核告诉我们,连接我们的客户端中,有哪些客户端与我们进行通信。
第一种:select/poll——遍历线性表来检测谁跟我们通信了
只会告诉我有几个客户端与我们通信了,分别是谁,我们不知道。我们需要把所有连接的客户端去进行遍历,找出都有谁跟我通信了。
第二种:epoll——遍历红黑树来检测谁跟我们通信了
不仅告诉我们几个给我们通信了,还会告诉我们跟我们通信的客户端都是谁。
什么是I/O多路转接技术:
内核需要检测与文件描述符有关的表,检测文件描述符的读缓冲区,因为读缓冲区有东西则证明有人发数据过来了。
select的参数和返回值
函数原型:——返回值是文件描述符中有几个发生了变化,是int类型
参数
- nfds: 要检测的文件描述中最大的fd+1 ——nfds的最大值是1024,nfds在内核中用数组来实现的
- readfds: 读集合
- writefds: 写集合
- exceptfds: 异常集合
- timeout: 阻塞时间
文件描述符集类型:
fd_set rdset;
文件描述符操作函数:(函数的使用范例在下方图片中绿色字所示)重点
- 全部清空
- void FD_ZERO(fd_set *set);
- 从集合中删除某一项
- void FD_CLR(int fd, fd_set *set);
- 将某个文件描述符添加到集合
- void FD_SET(int fd, fd_set *set);
- 判断某个文件描述符是否在集合中
- int FD_ISSET(int fd, fd_set *set);
使用select函的优缺点:
- 优点:
- 跨平台
- 缺点:
- 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
- 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
- select支持的文件描述符数量太小了,默认是1024
为什么默认描述符数量是1024?
下图所示是fd_set的内核代码片段截图,通过截图可以看到fd_set实际上是_kernel_fd_set结构体,而结构体里面只有一个数组,因此fd_set实际上是一个数组,数组的长度是__FDSET_LONGS,查看__FDSET_LONGS的定义可知,他的大小等于,定义出的FD_SETSIZE宏的大小是1024,_NFDBITS的大小是
算一下,1024/(8*(32位则unsigned long的值为4, 64位则unsigned long的值为8,我们假定是64位))=16=__FDSET_LONGS,因此数组大小是16,类型是long,64位系统中long是8字节,每个字节8位,
因此fd_set的二进制标志位数量就是:16*8*8=1024
fd_set的内核代码片段截图
因为selete的局限性是只能委托1024个。
当被监听的描述符发生变化,就会返回。
select函数是如何工作的
假定客户端A,B,C,D,E, F连接到服务器,分别对应文件描述符 3, 4,100,101,102,103
因此我们要通过调用select函数通过内核来判断链接上来的客户端是否给我发送数据了,
第一步——创建一个fd_set类型的表
fd_set reads, temp; - 用户空间
//temp用于作为select传入的参数,因为select函数会把传入的参数的值修改掉,具体原因往下看
第二步——文件描述符添加到待检测的表中
FD_SET(3, &reads);
FD_SET(4, &reads);
以此类推,全都写一遍
第三步——调用select函数
select(103+1 //最大的文件描述符+1
&reads, //读集合中的地址
NULL, //写
NULL, //异常
NULL //时延
)
调用函数之后,内核就开始干活了。
内核 先把参数reads的内容从用户空间复制一份表到内核空间,然后依次遍历复制过来的表,如果该二进制位的值为1,说面该标志位是一个客户端连接的文件描述符,则去查看内核缓冲区的read空间中是否有数据,如果有数据,则该位置的值不变,如果没有数据,则将该位置的值置为0.(内核检测完成之后,修改了fd_set表)
比如ABC发送了数据,则
修改之后,这份表又会从内核区拷贝到用户区,新拷贝过来的表会覆盖我的 &reads ,因此调用select的时候,传入的 &reads 应该是原始数据的拷贝
我们再去遍历 &reads 就能找到哪些客户端发送了数据给我,select函数的返回值只是告诉你有几个客户端发送数据。
程序中使用select函数的伪代码
select的io转接是异步的。
通过下面伪代码可以看出,当判断完新连接之后,要在下一轮while循环中才看去读取客户端是否给我发送数据。当我们获知新的链接的同时,客户端给我们发送了数据我们是无法实时的知道的,客户端发过来的数据此时还暂时的存储在内核read缓冲区中。
int main(){
int lfd = socket();//创建一个用于监听的套接字
bind(); //绑定
listen(); //监听
// 创建一文件描述符表
fd_st reads, temp;
// 初始化
fd_zero(&reads);
// 监听的lfd加入到读集合
//监听的文件描述符不负责数据发送,那么他如何知道别人跟他建立链接了?
//listen函数处理SYN,这也算是收发数据。如果有连接请求,就会发到文件描述符的读缓冲区了,
//因此监听的文件描述符也需要内核帮我们去检测的。如果有新的连接,则会有数据
fd_set(lfd, &reads);
//获取最大的文件描述符
int maxfd = lfd;
while(1){
// 委托内核检测
//拷贝文件描述符表
temp = reads;
//调用select函数,获取有几个客户端发送了数据给我
int ret = select(maxfd+1, &temp, NULL, NULL, NULL);
// 判断监听描述符的标志位是否为1,如果是1,则说明有新连接
if(fd_isset(lfd, &temp)){
// 接受新连接(此时accept不会阻塞,因为进入这个if,说明真的有新连接)
int cfd = accept();
// cfd加入我们要检测的读集合
//读的是reads,因为reads存的是原始数据
fd_set(cfd, &reads);
// 新来的客户端连接的文件描述符有可能会是最大的
//因此要更新记录最大文件描述符的变量值maxfd
maxfd=maxfd<cfd ? cfd:maxfd;
}
// 不是有新连接,说明此时在通信
//遍历判断哪个文件描述符绑定的客户端发送了数据
for(int i=lfd+1; i<=maxfd;++i){
if(fd_isset(i, &temp){
int len = read();
//说明对方断开连接
if(len == 0){
// cfd 从读集合中删除
fd_clr( i, &reads);
}
write();
}
}
}
}
select的实现例子
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/select.h>
#include<errno.h>
int main(int argc, char *argv[]){
if(argc<2){
printf("eg: ./a.out port \n");
exit(1);
}
struct sockaddr_in serv_addr;
socklen_t serv_len=sizeof(serv_addr);
int port=atoi(argv[1]);
//创建套接字
int lfd=socket(AF_INET, SOCK_STREAM, 0);
//初始化服务器
memset(&serv_addr, 0, serv_len);
//地址族
serv_addr.sin_family=AF_INET;
//监听本地所有IP
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
//设置端口
serv_addr.sin_port=htons(port);
//绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
//设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept .... \n");
//使用信号回收子进程pcb
//不然让父进程一直用wait循环等待,那父进程就什么都做不了了
struct sigaction act;
act.sa_handler=recyle;
act.sa_flags=0;
//初始化mask,不然局部变量里面有什么我们不确定
sigemptyset(&act.sa_mask);
//规定我们要接受的信号
sigaction(SIGCHLD, &act, NULL);
struct sockaddr_in client_addr;
socklen_t cli_len=sizeof(client_addr);
//最大文件描述符
int maxfd=lfd;
//文件描述符读集合
fd_set reads, temp;
//初始化reads为0
FD_ZERO(&reads);
//把lfd放在读集合里,让内核帮忙检测是否有新的连接请求
FD_SET(lfd, &reads);
while(1){
//委托内核做IO检测
temp=reads;
//select函数是跨平台的,在linux下,第一个参数一定要写对
//在windows下,第一个参数你写-1,写谁都行
int ret=select(maxfd+1, &temp, NULL, NULL, NULL);
if(ret==-1){
perror("select error");
exit(1);
}
//有客户端发起新连接
if(FD_ISSET(lfd, &temp)){
//返回值是1,说明有新的连接
//接受连接请求——accept不阻塞
int cfd=accept(lfd,
(struct sockaddr*)&client_addr, &cli_len);
if(cfd==-1){
perror("accept error");
exit(1);
}
char ip[64];
printf("new client IP:%s, PORT: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip,sizeof(ip)),
ntohs(client_addr.sin_port));
//将cfd加入到待检测的读集合,这样下一次就能检测到了
FD_SET(cfd, &reads);
//更新最大描述符
maxfd=maxfd<cfd?cfd:maxfd;
}
//已经连接上的客户端给我发送了数据
//遍历read,去看
for(int i=lfd+1; i<=maxfd; ++i){
//判断temp,因为temp是被内核修改过的表
if(FD_ISSET(i, &temp)){
char buf[1024]={0};
int len=recv(i, buf, sizeof(buf), 0);
if(len==-1){
perror("recv error");
exit(1);
}else if(len ==0){
printf("客户端已经断开连接\n");
close(i);
//从读集合删除
FD_CLR(i, &reads);
}else{
//正常的手法数据
printf("recv buf: %s\n", buf);
//把数据发出去
//不把\0发出去的前提条件是buf已经被初始化了
//如果buf没初始化,则需要把\0发出去,否则接收端
//收到的数据的后边是乱码
send(i, buf, strlen(buf), 0);
}
}
}
}
close(lfd);
return 0;
}
poll函数
poll可以突破1024,因为他内部实现应用的是链表。
select有读写异常者三种行为,三种行为做了三张表。poll把读写异常者三种状态封装到一个结构体里面,将三张表融合了。
函数原型(select的和poll的):
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
};
int poll(struct pollfd *fd, //结构体数组首地址,用数组,因为链接客户端的文件描述符不能只是一个
nfds_t nfds, //被typedef重新定义的数据类型,看成int类型就可以,功能和select的nfds差不多
int timeout);
- pollfd -- 数组的地址
- nfds: 数组的最大长度, 数组中最后一个使用的元素下标+1
- 内核会轮询检测fd数组的每个文件描述符
- timeout:
- -1: 永久阻塞
- 0: 调用完成立即返回
- >0: 等待的时长毫秒
- 返回值: iO发生变化的文件描述符的个数
你想监听哪个事件(读写异常),就把相应的时间赋值给结构体的events变量。
如果你读写异常三个都想监听,该如何设置?
模仿open函数,利用按位或
short是两个字节,16位,我们对相应的位赋值即可,不用像select一样去弄很多表。
内核给的反馈也赋值给了结构体。
下表看框起来的东西就行了
poll的实现代码(不要求会写,会select就可以了)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <poll.h>
#define SERV_PORT 8989
int main(int argc, const char* argv[]){
int lfd, cfd;
struct sockaddr_in serv_addr, clien_addr;
int serv_len, clien_len;
// 创建套接字
lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(SERV_PORT); // 设置端口
serv_len = sizeof(serv_addr);
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
// poll结构体
struct pollfd allfd[1024];
//最后一个有效元素下标
int max_index = 0;
// 初始化数组,fd的值为-1,说明没被占用
for(int i=0; i<1024; ++i){
allfd[i].fd = -1;
}
//把用于监听的描述符加入到待检测的数组里面
allfd[0].fd = lfd;
//初始化事件 POLLIN指的是读事件
allfd[0].events = POLLIN;
while(1){
int i = 0;
int ret = poll(allfd, max_index+1, -1);
if(ret == -1){
perror("poll error");
exit(1);
}
// 判断是否有连接请求
//按位与操作,16位的数如果里面有POLLIN,则&的结果为1
if(allfd[0].revents & POLLIN){
clien_len = sizeof(clien_addr);
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&clien_addr, &clien_len);
printf("============\n");
// cfd添加到poll数组
for(i=0; i<1024; ++i){
if(allfd[i].fd == -1){
allfd[i].fd = cfd;
break;
}
}
// 更新最后一个元素的下标
max_index = max_index < i ? i : max_index;
}
//有客户端发数据了,则遍历数组来接收数据
for(i=1; i<=max_index; ++i){
int fd = allfd[i].fd;
//说明此位置是无效位置
if(fd == -1){
continue;
}
if(allfd[i].revents & POLLIN){
// 接受数据
char buf[1024] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1){
perror("recv error");
exit(1);
}else if(len == 0){
allfd[i].fd = -1;
close(fd);
printf("客户端已经主动断开连接。。。\n");
} else {
printf("recv buf = %s\n", buf);
for(int k=0; k<len; ++k){
//小写转大写
buf[k] = toupper(buf[k]);
}
printf("buf toupper: %s\n", buf);
send(fd, buf, strlen(buf)+1, 0);
}
}
}
}
close(lfd);
return 0;
}