1. 概念澄清
1.1
A socket is ready for reading if any of the following four conditions is true:
- The read half of the connection is closed (i.e., a TCP connection that has received a FIN). A read operation on the socket will not block and will return 0 (i.e., EOF)
A socket is ready for writing if any of the following four conditions is true:
- The write half of the connection is closed. A write operation on the socket will generate SIGPIPE (Section 5.12).
StackOverflow上关于这个问题给出了答案,其实一开始看到这个问题,难以理解的就是shutdown(xxxx,SHUT_RD)
,一端调用该函数代表其不再接收数据,但是如果假设active close
的一端首先调用该函数,显然是无法理解的,因为一般都是发送数据方作为active close
端,而作为passive close
端调用该函数,又多此一举,在百科中说该函数调用为空操作不权威,这个问题先按照常见的一种情形处理:
1.2
TCP的状态变化图回顾:
从理论上分析首先client处于FIN_WAITE_1
,之后接收到server的ACK
后,变化到FIN_WAITE_2
,等待server的FIN
;server此时接收到client的FIN
后处于CLOSE_WAIT
状态,之后其调用close
向client发送FIN
,自己进入LAST_ACK
,等待client的ACK
。
腾讯的面经会有提问TIME_WAIT
的作用,现在回顾如下:
- To implement TCP’s full-duplex connection termination reliably
TIME_WAIT
状态用来重复发送ACK
,假设第一次FIN_WAIT_2
回复的ACK
丢失的话,该TIME_WAITE
状态持续2MSL
。- To allow old duplicate segments to expire in the network
- 相同地址和端口号建立的连接不希望接收到
lost duplicate
,也就是上次连接的数据,则使用该状态让当前连接的数据全部过期。
2. Select server和Client的改进代码
2.1 Server
//6_ser.c
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <err.h>
#define INET_ADDRSTRLEN 16
#define LISTENQ 10
#define SERV_PORT 9877
#define MAXLINE 20
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void str_echo(int sockfd);
ssize_t writen(int fd, const void *vptr, size_t n);
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
char strcliaddr[INET_ADDRSTRLEN];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
handle_error("socket");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if(bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1)
handle_error("bind");
if(listen(listenfd, LISTENQ) == -1)
handle_error("listen");
maxfd = listenfd; /* initialize */
maxi = -1; /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; ) {
rset = allset; /* structure assignment */
if((nready=select(maxfd+1, &rset, NULL, NULL, NULL)) == -1)
handle_error("select");
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen)) == -1 )
handle_error("accept");
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE)
errx(1,"too many clients");
if(inet_ntop(AF_INET,&cliaddr.sin_addr,strcliaddr,INET_ADDRSTRLEN) == NULL)
handle_error("inet_pton");
printf("connected client number : %d, ipaddress:%s\n",i,strcliaddr);
FD_SET(connfd, &allset); /* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = read(sockfd, buf, MAXLINE)) < 0)
handle_error("read");
if(n == 0){
if(getpeername(sockfd,(struct sockaddr *) &cliaddr, &clilen)== -1)
handle_error("getpeername");
if(inet_ntop(AF_INET,&cliaddr.sin_addr,strcliaddr,INET_ADDRSTRLEN) == NULL)
handle_error("inet_pton");
printf("closed client number : %d, ipaddress:%s\n",i,strcliaddr);
if(close(sockfd) == -1)
handle_error("close");
if(i == maxi)
maxi-=1;
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
if(writen(sockfd, buf, n) == -1)
handle_error("writen");
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
if(writen(sockfd, buf, n) == -1)
handle_error("writen");
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
errx(1,"str_echo: read error");
}
ssize_t writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ( (nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return (-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return (n);
}
2.2 Client
//6_cli.c
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <err.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#define SERV_PORT 9877
#define MAXLINE 20
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
ssize_t writen(int fd, const void *vptr, size_t n);
void str_cli(FILE *fp, int sockfd);
int max(int,int);
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
errx(1,"usage: tcpcli <IPaddress>");
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
handle_error("socket");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) == -1)
handle_error("inet_pton");
if(connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1)
handle_error("connect");
str_cli(stdin, sockfd);
exit(0);
}
int max(int a,int b)
{
return a < b? b:a;
}
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char buf[MAXLINE];
int n;
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; ) {
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
if(select(maxfdp1, &rset, NULL, NULL, NULL) == -1)
handle_error("select");
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if ( (n = read(sockfd, buf, MAXLINE)) < 0)
handle_error("read");
if( n == 0){
if (stdineof == 1)
return; /* normal termination */
else
errx(1,"str_cli: server terminated prematurely");
}
if( write(fileno(stdout), buf, n) == -1 )
handle_error("write stdio");
}
if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */
if ( (n = read(fileno(fp), buf, MAXLINE)) < 0)
handle_error("read");
if(n == 0){
stdineof = 1;
if(shutdown(sockfd, SHUT_WR)== -1)
handle_error("shutdown");/* send FIN */
FD_CLR(fileno(fp), &rset);
continue;
}
if(writen(sockfd, buf, n) == -1)
handle_error("writen");
}
}
}
ssize_t writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0)
{
if ( (nwritten = write(fd, ptr, nleft)) <= 0)
{
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return (-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return (n);
}
3. 结果验证
//server端
[root@localhost ~]# ./6_ser
connected client number : 0, ipaddress:127.0.0.1
connected client number : 1, ipaddress:192.168.182.130
closed client number : 0, ipaddress:127.0.0.1
closed client number : 1, ipaddress:192.168.182.130
//client0
[root@localhost ~]# ./6_cli 127.0.0.1
this is the first client.
this is the first client.
//CTRL+D
[root@localhost ~]#
//client1
[root@localhost ~]# ./6_cli 192.168.182.130
this is the second client.
this is the second client.
//CTRL+D
[root@localhost ~]#
以上两个client同时连接到server,但是该server是single loop,意味着对于client数据量比较大的时候,阻塞其他client接受server的服务。然后看看状态转化,以一个client为例:
//server继续等待,此时上面两个连接已经结束,9877为server的端口。
[root@localhost ~]# netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
//笔者是同一台电脑多个终端,所以这里看到两个建立连接的socket,59750是clinet的端口
[root@localhost ~]# ./6_cli 192.168.182.130
//等待输入
[root@localhost ~]# ./6_ser
connected client number : 0, ipaddress:127.0.0.1
connected client number : 1, ipaddress:192.168.182.130
closed client number : 0, ipaddress:127.0.0.1
closed client number : 1, ipaddress:192.168.182.130
connected client number : 0, ipaddress:192.168.182.130 //上面新连接的client
//此时观察netstat
[root@localhost ~]# netstat -a | grep tcp
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:ssh 0.0.0.0:* LISTEN
tcp 0 0 localhost.localdom:9877 localhost.localdo:59750 ESTABLISHED
tcp 0 0 localhost.localdo:59750 localhost.localdom:9877 ESTABLISHED
//在client键入EOF,迅速查看结果,client的TCP处于TIME_WAIT状态,server端的socket已经关闭
[root@localhost ~]# netstat -a | grep tcp
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost.localdo:59750 localhost.localdom:9877 TIME_WAIT
4.总结
client和server都采用select
处理,为了在server显示连接的client,及client结束时打印相应的信息,笔者用了inet_pton
和getpeername
,对于maxi
的处理,笔者只是简单的让其减一。最后这两个代码本身的缺陷,在以前的章节有讨论过,在此不赘述。