TCP/IP协议栈 -- 编写较稳定的client注意的细节

这张主要以实验形式观察一个简单的TCP 连接
实验代码如下:

#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#define PORT 6666
#define MAXSIZE 1024
void str_cli(FILE *, int);
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "./client IP\n");
        return 1;
    }
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(PORT);
    inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
    connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    //FILE * fp = fopen("./aa","r");
    //str_cli(fp, fd);
    sleep(60);
    close(fd);
    exit(0);
}

void str_cli(FILE * fp, int fd)
{
    char sendbuff[MAXSIZE], recvbuff[MAXSIZE];
    bzero(sendbuff, MAXSIZE);
    bzero(recvbuff,MAXSIZE);
    while (fgets(sendbuff, MAXSIZE, fp) != NULL)
    {
        write(fd, sendbuff, strlen(sendbuff));
        printf("hello\n");
        /*  if(read(fd, recvbuff, MAXSIZE) == 0)
        {   
            if(errno == ECONNRESET)
            {
                fprintf(stderr, "reconnect\n");     
            }
            fprintf(stderr, "server terminated!\n");
            exit(1);
        }*/
        fputs(recvbuff, stdout);
        bzero(sendbuff, MAXSIZE);
        bzero(recvbuff,MAXSIZE);
    }
}
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#define PORT 6666
#define MAXSIZE 1024
void str_ser(int);
int main()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr, clientaddr;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //inet_aton("15.15.182.182",&serveraddr.sin_addr);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(PORT);
    bind(fd,(const struct sockaddr *)&serveraddr, sizeof(serveraddr));
    listen(fd, 5);
    socklen_t len = sizeof(clientaddr);
    for(;;)
    {
    //  int clientfd = accept(fd, (struct sockaddr *)&clientaddr, &len);
    /*  if(fork() == 0)
        {
            close(fd);
            str_ser(clientfd);
            exit(0);
        }
        */
    //  close(clientfd);
        sleep(3);
    }
    return 0;
}

void str_ser(int clientfd)
{
    char buff[MAXSIZE];
    size_t n;
    bzero(buff, MAXSIZE);
    sleep(2000);
    while( (n = read(clientfd, buff, MAXSIZE)) > 0)
    {

            write(clientfd, buff, n);
            bzero(buff, MAXSIZE);
    }
    if(n <= 0)
        fprintf(stderr, "read error\n");

}

1. 客户端连接一个没有在监听状态的server
我们这里先将server的accept注释掉,这里可以看到 三次握手在listen时就可以完成。

me#tcpdump -i eth0 -e -vv tcp port 6666 and host 192.168.1.10
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
12:30:24.949570 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 20312, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.2.58726 > 192.168.1.10.6666: Flags [S], cksum 0x69bc (correct), seq 1504439162, win 29200, options [mss 1460,sackOK,TS val 1942562 ecr 0,nop,wscale 7], length 0
12:30:24.949834 00:0c:29:c9:00:60 (oui Unknown) > 00:0c:29:61:1a:9b (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.10.6666 > 192.168.1.2.58726: Flags [S.], cksum 0x0f76 (correct), seq 979940964, ack 1504439163, win 28960, options [mss 1460,sackOK,TS val 2124344 ecr 1942562,nop,wscale 7], length 0
12:30:24.949939 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 66: (tos 0x0, ttl 64, id 20313, offset 0, flags [DF], proto TCP (6), length 52)
    192.168.1.2.58726 > 192.168.1.10.6666: Flags [.], cksum 0xae7d (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 1942562 ecr 2124344], length 0

那么这时我们把server关掉 或者把listen注释掉

ktop# tcpdump -i eth0 -e -vv tcp port 6666 and host 192.168.1.10
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
12:35:31.540964 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 53359, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.2.58728 > 192.168.1.10.6666: Flags [S], cksum 0xa0da (correct), seq 3266781157, win 29200, options [mss 1460,sackOK,TS val 2019210 ecr 0,nop,wscale 7], length 0
12:35:31.541212 00:0c:29:c9:00:60 (oui Unknown) > 00:0c:29:61:1a:9b (oui Unknown), ethertype IPv4 (0x0800), length 60: (tos 0x0, ttl 64, id 27112, offset 0, flags [DF], proto TCP (6), length 40)
    192.168.1.10.6666 > 192.168.1.2.58728: Flags [R.], cksum 0x4a63 (correct), seq 0, ack 3266781158, win 0, length 0

这里可以看到client向server发送了SYN,server并没有相应的端口号在监听,所以内核就会主动断掉这个链接并向client发送RST复位断掉连接(这个可以由client select 检查描述符状态并由read返回0读取到)只需在client.c 中加入如下代码:

    char buff[256]={0};
    fd_set rdf;
    FD_ZERO(&rdf);
    FD_SET(fd, &rdf);
connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
    int nready = select(fd+1, &rdf, NULL, NULL, 0);
    if(nready)
    {
        if(FD_ISSET(fd, &rdf) && errno == ECONNREFUSED)
        {
            if(read(fd, buff, 256) <= 0)
                printf("RST?\n");
        }
    }

并包含头文件sys/select.h 对于FIN 和RST 都可以用read读取 但是这里还不能肯定是收到了RST复位。 不用担心这里还有errno 当client connect失败的时会返回相应的错误。errno == ECONNREFUSED 就表示里对端没有在监听。
2. 连接的server并不存在
我们用client连接一个不存在的网络 这里抓包可以看到如下结果:

    192.168.1.2.52002 > 18.16.15.15.6666: Flags [S], cksum 0xb88a (correct), seq 4112007910, win 29200, options [mss 1460,sackOK,TS val 2628936 ecr 0,nop,wscale 7], length 0
13:16:10.444400 00:0c:29:61:1a:9b (oui Unknown) > 00:1e:2a:67:f3:2e (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 48151, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.2.52002 > 18.16.15.15.6666: Flags [S], cksum 0xb88a (correct), seq 4112007910, win 29200, options [mss 1460,sackOK,TS val 2628936 ecr 0,nop,wscale 7], length 0
13:16:42.540099 00:0c:29:61:1a:9b (oui Unknown) > 00:1e:2a:67:f3:2e (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 48152, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.2.52002 > 18.16.15.15.6666: Flags [S], cksum 0x9932 (correct), seq 4112007910, win 29200, options [mss 1460,sackOK,TS val 2636960 ecr 0,nop,wscale 7], length 0
13:16:42.540578 00:0c:29:61:1a:9b (oui Unknown) > 00:1e:2a:67:f3:2e (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 48152, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.1.2.52002 > 18.16.15.15.6666: Flags [S], cksum 0x9932 (correct), seq 4112007910, win 29200, options [mss 1460,sackOK,TS val 2636960 ecr 0,nop,wscale 7], length 0

这里可以看到client会不断的发送SYN 握手 每次间隔时间并不相同, 一段时间后client就会放弃连接。根据不同系统的不同实现, client connect 坚持时间并不同。这里connect函数 虽说有个errno==ENETUNREACH,但貌似并没有得到相应结果。这时, 可以对connect进行定时alarm。当connect返回成功时再将定时器设置为0. 如果长时间不能连接, 就是不能到达。也可以将套接字设置为非阻塞, 大致如下connect返回processing 配合select 当套接字变为可写时, getsockopt 得到套接字状态:

SetNONBlock();
        tval.tv_sec = timeout;
        tval.tv_usec = 100;
        fd_set wfd;
        FD_ZERO(&wfd);
        FD_SET(sockfd, &wfd);
        int resconn = connect(sockfd, (const sockaddr *)&seraddr, sizeof(seraddr));
        if(resconn == 0)
        {
            write(1, "Connection success.\n",50);
            return true;
        }
        if (resconn == -1)
        {
            if(errno == EINPROGRESS)
            {
                int nready = select(sockfd+1, NULL, &wfd, NULL, &tval);
                if(nready == -1)
                {
                    close(sockfd);
                    HandleError("Select");
                }else if(nready == 0)
                {
                    write(2, "Connect Timeout!\n", 50);
                    close(sockfd);
                    exit(0);
                }else
                {
                    int err;
                    socklen_t len = sizeof(err);
                    if(FD_ISSET(sockfd, &wfd))
                    {
                        if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (void *)&err, &len) == -1)
                        {
                            close(sockfd);
                            HandleError("Getsockopt");
                        }
                        if(err == 0)
                        {
                            write(1, "Connect success.\n", 50);
                            return true;
                        }else{
                            close(sockfd);
                            errno = err;
                            HandleError("Connect");
                        }

3.client 处于连接状态 server 进程崩溃 被kill 掉。
这里让client连接server后不发生数据, 把 server 进程kill掉。抓包结果。

    192.168.1.10.6666 > 192.168.1.2.56076: Flags [F.], cksum 0x7984 (correct), seq 1, ack 1, win 227, options [nop,nop,TS val 2808762 ecr 426453], length 0
    12:43:54.180737 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 66: (tos 0x0, ttl 64, id 22499, offset 0, flags [DF], proto TCP (6), length 52)
            192.168.1.2.56076 > 192.168.1.10.6666: Flags [.], cksum 0x4fc9 (correct), seq 1, ack 2, win 229, options [nop,nop,TS val 437134 ecr 2808762], length 0

这里可以看到收到了server发送的FIN断开连接报文。也试着在server中插入assert来abort程序 还是可以收到FIN 。 这样的话 对于client都可以用select 检查套接字可读并且read == 0 若这里还是不能确定是FIN 可以在向套接字里面write 这是errno == EPIPE

EPIPE fd is connected to a pipe or socket whose reading end is closed.
When this happens the writing process will also receive a SIG-
PIPE signal. (Thus, the write return value is seen only if the
program catches, blocks or ignores this signal.)

4.client 处于连接状态 server 主机断电

14:12:26.513960 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 9905, offset 0, flags [DF], proto TCP (6), length 60)
            192.168.1.2.56079 > 192.168.1.10.6666: Flags [S], cksum 0xa9e4 (correct), seq 4199008720, win 29200, options [mss 1460,sackOK,TS val 1765217 ecr 0,nop,wscale 7], length 0
            14:12:26.514225 00:0c:29:c9:00:60 (oui Unknown) > 00:0c:29:61:1a:9b (oui Unknown), ethertype IPv4 (0x0800), length 74: (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
            192.168.1.10.6666 > 192.168.1.2.56079: Flags [S.], cksum 0xfe93 (correct), seq 41519603, ack 4199008721, win 28960, options [mss 1460,sackOK,TS val 4136836 ecr 1765217,nop,wscale 7], length 0
            14:12:26.514299 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 66: (tos 0x0, ttl 64, id 9906, offset 0, flags [DF], proto TCP (6), length 52)
            192.168.1.2.56079 > 192.168.1.10.6666: Flags [.], cksum 0x9d9b (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 1765217 ecr 4136836], length 0

这里可以看到 client与server 三次握手完成后 server突然断电, client 并不能知道, 这时就需要TCP的保活计时器, 默认情况下是俩小时,所以client要想知道server已经不存在 那么就是两小时后了。 这个可以自己定义应用层的保活机制, 定时向server 发送东西 确保server存在。

5.client 正在发送东西, server 主机断电

12:51:46.167509 00:0c:29:61:1a:9b (oui Unknown) > 00:0c:29:c9:00:60 (oui Unknown), ethertype IPv4 (0x0800), length 77: (tos 0x0, ttl 64, id 58281, offset 0, flags [DF], proto TCP (6), length 63)
192.168.1.2.49802 > 192.168.1.10.6666: Flags [P.], cksum 0x1ae7 (correct), seq 100:111, ack 1, win 229, options [nop,nop,TS val 409376 ecr 3143790], length 11

这里可以看到 client 此时已经收不到server的ACK确认了 但是client 还是可以write,这是因为write只是向内核发送缓冲区里面去写, 当缓冲写满时(一般时间并不短) write就会返回EPIPE。 这种情况下,如果与server之间没有保活确认 这样的话就会有问题。

6.client 正在发送东西, server 被kill 掉
这里的抓包结果如下:

    192.168.1.2.49800 > 192.168.1.10.6666: Flags [P.], cksum 0xd4e5 (correct), seq 166:177, ack 2, win 229, options [nop,nop,TS val 305519 ecr 3046651], length 11
12:44:50.740830 00:0c:29:c9:00:60 (oui Unknown) > 00:0c:29:61:1a:9b (oui Unknown), ethertype IPv4 (0x0800), length 60: (tos 0x0, ttl 64, id 19012, offset 0, flags [DF], proto TCP (6), length 40)
    192.168.1.10.6666 > 192.168.1.2.49800: Flags [R], cksum 0x263a (correct), seq 2864217854, win 0, length 0

可以看到 收到了server的RST 复位 , 这里如果client还要一直写的话 就是产生SIGPIPE信号 默认情况下 client 会终止 这里可以 用可以重新写中断函数 继续在去重新建立connect。

综上所述: 书写一个稳定的client需要注意的地方还是很多的, 上面的这几点都有相应的解决方案。server的任何状态都影响的client 比如还有server并不read client的发送, client在三次握手完成后立即向server 发送RST复位 accept什么时候去取三次握手完成的客户端等等 后续我们还会讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值