网络编程之I_O Multiplexing总结(四)

本文通过具体示例介绍如何使用select机制管理TCP连接,并展示了客户端和服务端的实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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_ptongetpeername,对于maxi的处理,笔者只是简单的让其减一。最后这两个代码本身的缺陷,在以前的章节有讨论过,在此不赘述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值