1、主动发起关闭连接
一开始是关闭状态,主动发起连接段发送SYN信号,进入SYN_SENT(主动打开状态),发送ACK和SYN信号,接收发送端返回的ACK信号,此时三次握手建立完毕,进入ESTABLISHED(数据传输状态)
一端发送FIN标志,进入FIN_WAIT_1状态,接收对端发送过来的ACK后进入FIN_WAIT2状态,(半关闭状态),主动发起端接收对端发来的FIN信号,然后发出回应ACK进入TIME_WAIT状态,对端接收ACK进入CLOSING。
进入TIME_WAIT后,会有2MSL等待时间,主要目的确保最后一个发送的ACK能够准确到达
主动关闭一端发出最后的一个ACK
ubuntu中的MSL时间大约是30秒,最大生成时间
2、被动发起,关闭连接
直接打开监听状态,接收客户端发送来的SYN,向客户端发送SYN和ACK,进入到SYN_RCVD,客户端接收ACK后进入ESTABLISHED状态,三次握手建立完毕
服务器端接收FIN发送ACK进入CLOSE_WAIT状态,发送FIN后进入LAST_ACK,接收完ACK后 关闭
可以通过netstat -apn | grep 端口号查看当前端口处于哪个状态
3、半关闭
是指,客户端断开连接,而服务器端仍然处于连接状态,此时,客户端无法发送数据,但是可以接收数据
RST是一个标志位,ACK应答发布过去的时候,就会发一个RST标志,三次握手建立失败,由对端的操作系统发送
半关闭:程序中如何实现半关闭:
当有多个文件描述符指向socket的时候,用close只能关闭一个,管的不彻底,所以就用到了shutdown
从程序的角度,可以使用API来控制实现半连接状态。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。
SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
shutdown不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
注意:
1.如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。
2.在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。
4、端口复用
会出现一种错误,当我们把服务器和客户端都打开的时候,先关闭服务器在关闭客户端,再次开启的时候就会出错,解决这个问题就可以使用端口复用。
端口复用
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
状态不会变,依然是CLOSE_WAIT,只不过改的是端口可以复用
出现错误的原因:
lient终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同
5、多路IO转换
多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
用内核来帮忙处理,可以省略连接时的阻塞,完成建立连接,读数据都不会有阻塞
6、多路IO转换的几种方法:
select函数,pool和epool函数
7、select函数原型分析
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
第一个参数:所监听的文件描述符中,最大的文件描述符+1,
fd_t是一个位图
第2/3/4参数:所监听的文件描述符"可读"事件
所监听的文件描述符"可写"事件
所监听的文件描述符"异常 "事件
第五个参数:设置一个参数 定时阻塞监控时间,3种情况
1.NULL,永远等下去
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
都是传入传出参数, 传进去的时候所有加进集合的所有文件描述符,传出的是满足条件的文件描述符
返回值:成功返回所监听的所有的监听集合中满足条件的总数。
void FD_CLR(int fd, fd_set *set); //fd从set中清除出去
int FD_ISSET(int fd, fd_set *set); //fd是否在集合中
void FD_SET(int fd, fd_set *set); //把fd设置到set集合中去
void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0
使用:
首先fd_set readfds;
FD_ZERO(&readfds)
FD_SET(fd1,&readfds)
FD_SET(fd2,&readfds)
FD_SET(fd3,&readfds)
select();返回一个总数
for(i<返回的总数)
FD_ISSET(放到,&readfds);判断在不在这个集合里面
弊端:
1、文件描述符1024个,slect监听最大的上限就是1024个
2、 当我们用的是3和1023时,查找是哪个满足条件时,会for从3到1024去查,我们自己定义一个数据结构,数组,把我们的放入数组中然后遍历这个数组
3、监听集合,满足监听条件的集合,是一个集合,需要将原有集合保存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 */
ssize_t n;
fd_set rset, allset; allset保存上一次的集合是什么样
char buf[MAXLINE];
char str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
socklen_t cliaddr_len;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20); /* 默认最大128 */
maxfd = listenfd; /* 初始化 */
maxi = -1; /* client[]的下标 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
for ( ; ; ) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* new client connection */有新的连接请求,链接的那个发了数据过来,
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++) {
if (client[i] < 0) { //一开始把这个数组的值全部变为-1了,找一个任意地让把connfd放进去
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
}
/* 达到select能监控的文件个数上限 1024 */
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 添加一个新的文件描述符到监控信号集里 */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 更新client[]最大下标值 */
if (--nready == 0)
continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,
负责处理未处理完的就绪文件描述符 */
}//判断是否有新的链接,并且把新的文件描述符加到集合里
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ( (sockfd = client[i]) < 0) continue;
if (FD_ISSET(sockfd, &rset)) {//判断已连接的有没有读事件发生,没有的话回到死循环处接着监听,这里的Sockfd是fd1 已经被踢出去了,如果有数据发送,就不会踢出去,接着向下执行
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
Close(sockfd); /* 当client关闭链接时,服务器端也关闭对应链接 */
FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
client[i] = -1;
} else {
int j;
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
}
if (--nready == 0)
break;
}
}
}
close(listenfd);
return 0;
}
select实现
文件描述符分两大类
一、建立链接完成 : 读,写,异常
二、建立连接请求:listenfd
一开始,rest有两个值listenfd fd1,当resect返回的时候,他返回对应时间发生的的,listen事件有事件发生 sd1没有进行数据传输所以会把fd1踢出去
在rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
之后,如果你在rest里面的几个文件有处理的就去处理,没有的就踢掉
8
poll函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
nfds 监听的个数
timeout 毫秒级等待
-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
0:立即返回,不阻塞进程
>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值
fds:数组的首地址,结构体数组
数组里面元素的个数
优点: 突破1024限制,通过修改配置文件
监听集合返回集合 分离
搜索范围小,定义一个数组,满足条件的放到一个数组里面
epoll可以实现,监听100个文件,有三个满足的,直接告诉你是哪三个
突破1024限制,通过修改配置文件
可以使用cat命令查看一个进程可以打开的socket描述符上限。
cat /proc/sys/fs/file-max
如有需要,可以通过修改配置文件的方式修改该上限值。
sudo vi /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如下图所示。
* soft nofile 65536 下限
* hard nofile 100000 上限
监听集合返回集合 分离
fds[0].fd = lkistenfd;
fds[0].events = POOLIN /POLLOUT/POLLERR
fd[0].revents 系统给填
struct pollfd fds[50000];
fds[1].fd = d1
fd[1].events = POILLIN
pool(fds,5,.-1)
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <errno.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main(int argc, char *argv[])
{
int i, j, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
client[0].fd = listenfd;
client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
maxi = 0; /* client[]数组有效元素中最大元素下标 */
for ( ; ; ) {
nready = poll(client, maxi+1, -1); /* 阻塞 */
if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
break;
}
}
if (i == OPEN_MAX)
perr_exit("too many clients");
client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
if (i > maxi)
maxi = i; /* 更新client[]中最大元素下标 */
if (--nready <= 0)
continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
}
for (i = 1; i <= maxi; i++) { /* 检测client[] */
if ((sockfd = client[i].fd) < 0)
continue;
if (client[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) { /* 当收到 RST标志时 */
/* connection reset by client */
printf("client[%d] aborted connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
perr_exit("read error");
}
} else if (n == 0) {
/* connection closed by client */
printf("client[%d] closed connection\n", i);
Close(sockfd);
client[i].fd = -1;
} else {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Writen(sockfd, buf, n);
}
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
return 0;