【一】tinyWebServer实战:socket讲解+抓包分析

949 阅读1分钟

功能

telnet连接server,server向telnet发送hi,然后主动断开。

服务端:

[hqinglau@centos 01-015]$ ./a.out 172.21.16.6 12347
connect 42.193.121.128: 47282
now exit...

这边的IP为172.21.16.6是因为我用的云服务器,ifconfig查出来是这个ip。但是外网ip可用42.193.121.128。

linux 测试:

[hqinglau@centos ~]$ telnet 42.193.121.128 12347
Trying 42.193.121.128...
Connected to 42.193.121.128.
Escape character is '^]'.
hi
Connection closed by foreign host.

windows外网测试:

Microsoft Telnet> o 42.193.121.128 12347
正在连接到42.193.121.128...
hi
失去了跟主机的连接。
按任意键继续...

代码分析

参数设置,我们需要本机的ip,还有端口号。可以设置给使用者一个提示。

if (argc <= 2)
{
    printf("usage: %s ip_address port_number\n", basename(argv[0]));
    return EXIT_FAILURE;
}

提示如下:

[hqinglau@centos 01-015]$ ./a.out 
usage: a.out ip_address port_number

获取ip和port:

const char *ip = argv[1];
int port = atoi(argv[2]);

sockaddr_in定义如下:

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};

sin_family

用到的三个为:sin_family表示协议,sin_port表示端口,sin_addr表示地址。

struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr);
address.sin_port = htons(port);

AF_INET(又称PF_INET)是 IPv4 网络协议的套接字类型,AF_INET6 则是 IPv6 的;而AF_UNIX 则是Unix系统本地通信。

选择AF_INET 的目的就是使用IPv4 进行通信。因为IPv4 使用32位地址,相比IPv6 的128位来说,计算更快,便于用于局域网通信。

而且AF_INET 相比 AF_UNIX 更具通用性,因为Windows上有AF_INET 而没有AF_UNIX。

int_pton将地址转化为数字,即presentation to numeric。如1.2.3.4或者localhost都可以。

然后创建socket。

int sock = socket(AF_INET,SOCK_STREAM,0);
assert(sock>=0); //断言

socket函数只用填协议类型和套接字类型(如:SOCK_STREAM流,SOCK_DGRAM报文),第三项不用填。

socket是一个连接的节点,创建好之后需要绑定地址端口就可以和其他节点相连进行通信。

int ret = bind(sock,(struct sockaddr *)&address,sizeof(address));
if(ret==-1)
{
    perror("bind error: ");
    return EXIT_FAILURE;
}

bind函数

至此,socket已经准备好了。

三次握手图如下:

三次握手

服务器端需要先listen,也就是监听端口才能接受client的请求。

ret = listen(sock,5);
assert(ret!=-1);

同样创建client的地址结构体。

struct sockaddr_in client;
socklen_t client_addrlen = sizeof(client);
int connfd = accept(sock,(struct sockaddr*)&client,&client_addrlen);
if(connfd<0)
{
    perror("accept error: ");
    return EXIT_FAILURE;
}

然后server便阻塞在accept函数,等待连接到来。经过三次握手连接成立(后面会讲)之后,便可以进行数据的传输。

为了方便,可以在服务器端显示一下client的信息。

char client_ip[32];
inet_ntop(AF_INET,&client.sin_addr,client_ip,sizeof(client_ip));
printf("connect %s: %d\n",client_ip,ntohs(client.sin_port));
printf("now exit...\n");

输出信息如下:

[hqinglau@centos 01-015]$ ./a.out 172.21.16.6 12347
connect 202.107.195.199: 6790
now exit...

尝试发送一条数据:

send(connfd,"hi\n",2,0);

代码连接:tinyWebServer

tcpdump抓包逐个分析

服务端:

connect 42.193.121.128: 51132
now exit...

tcpdump抓包指令:

[hqinglau@centos ~]$ sudo tcpdump -nn -X tcp port 12347

结果最后:

6 packets captured
6 packets received by filter
0 packets dropped by kernel

显示抓了6个包,下面依次分析。

抓包结果:

listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
12:31:41.718544 IP 202.107.195.199.5705 > 172.21.16.6.12347: Flags [S], seq 2459730644, win 64240, options [mss 1380,nop,wscale 8,nop,nop,sackOK], length 0

client端口5705对应server端口12347。这里我们只关注Flags,seq和ack。win是窗口大小,暂时不用管。

IP 202.107.195.199.5705为客户端,172.21.16.6.12347为服务器端。我用的云服务器,还有一个公网IP 42.193.121.128,二者表示同一个主机。

Tcpflags的值在手册里解释如下:

Tcpflags are some combination of S (SYN), F (FIN), P (PUSH), R (RST), U (URG), W (ECN CWR), E (ECN-Echo) or . (ACK), or none if no flags areset.

回顾tcp连接三次握手:

图片源自网络

很明显,Flags [S]这是client像server发送SYN,也就是第一次握手。seq为2459730644,那么下一次ack应该为2459730645才对。

12:31:41.718601 IP 172.21.16.6.12347 > 202.107.195.199.5705: Flags [S.], 
seq 3415114482, ack 2459730645, win 16060, options [mss 1460,nop,
nop,sackOK,nop,wscale 6], length 0

查看下一个包,果然如此!Flags [S.]表示是server向client发送SYN+ACK。

12:31:41.758324 IP 202.107.195.199.5705 > 172.21.16.6.12347: Flags [.], ack 1, win 1024, length 0

然后client向server发送Flags [.](表示ACK),至此,三次握手结束,client和server建立连接。

然后server会向client发送一个hi\n。

12:31:41.758487 IP 172.21.16.6.12347 > 202.107.195.199.5705: Flags [P.], seq 1:4, ack 1, win 251, length 3
	0x0000:  4500 002b e305 4000 4006 0d79 ac15 1006  E..+..@.@..y....
	0x0010:  ca6b c3c7 303b 1649 cb8e 82f3 929c 82d5  .k..0;.I........
	0x0020:  5018 00fb 479e 0000 6869 0a              P...G...hi.

最后的0a代表换行键(也是发送数据的一部分),很明显,显示发了三个字节hi\n。

Flags [P.]表示P (PUSH)。

发送数据完毕,server主动关闭端口退出。

我们常听说三次握手,四次挥手,可是就剩两个包了啊。

四次挥手(client <-> server)

这个流程是这样的:client觉得我可以裂开了,就发送一个FIN给server,server回应ACK表示我知道了,然后在合适的时候发送FIN给client,表示我也可以裂开了,client发送ACK表示知道了,二者完全断开。

所以,server主动关闭的情况下是只有两次的。如下:

12:31:41.758607 IP 172.21.16.6.12347 > 202.107.195.199.5705: Flags [F.], seq 4, ack 1, win 251, length 0
	0x0000:  4500 0028 e306 4000 4006 0d7b ac15 1006  E..(..@.@..{....
	0x0010:  ca6b c3c7 303b 1649 cb8e 82f6 929c 82d5  .k..0;.I........
	0x0020:  5011 00fb ba0e 0000                      P.......
12:31:41.797980 IP 202.107.195.199.5705 > 172.21.16.6.12347: Flags [.], ack 5, win 1024, length 0
	0x0000:  4568 0028 9562 4000 fb06 9fb6 ca6b c3c7  Eh.(.b@......k..
	0x0010:  ac15 1006 1649 303b 929c 82d5 cb8e 82f7  .....I0;........
	0x0020:  5010 0400 b709 0000                      P.......

Flags [F.]、Flags [.]即FIN, ACK。

进阶内容

sendfile

int fd = open(filename,O_RDONLY);
if(fd<0)
{
    perror("open file error: ");
    exit(EXIT_FAILURE);
}
struct stat stat_buf;
fstat(fd,&stat_buf);

sendfile(connfd,fd,NULL,stat_buf.st_size);

很明显,open file之后获取文件信息,用的是fstat,在sendfile函数中,主要用的是文件大小。

#include<sys/sendfile.h>
ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);

in_fd参数是待读出内容的文件描述符,out_fd参数是待写入内容的文件描述符。offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。count参数指定文件描述符in_fd和out_fd之间传输的字节数。

in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道。在Linux2.6.33之前,out_fd必须是一个socket,而从Linux2.6.33之后,out_fd可以是任何文件。

当需要对一个文件进行传输的时候,传统的read/write方式进行socket的传输具体流程细节如下:

1:调用read函数,文件数据copy到内核缓冲区 2:read函数返回,文件数据从内核缓冲区copy到用户缓冲区 3:write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区 4:数据从socket缓冲区copy到相关协议引擎。

在这个过程中发生了四次copy操作。

硬盘->内核->用户->socket缓冲区(内核)->协议引擎。

而sendfile的工作原理呢??

1、系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到 kernel buffer,然后数据被 kernel 直接拷贝到另外一个与 socket 相关的 kernel buffer。这里没有用户态和核心态之间的切换,在内核中直接完成了从一个 buffer 到另一个 buffer 的拷贝。 2、DMA 把数据从 kernel buffer 直接拷贝给协议栈,没有切换,也不需要数据从用户态和核心态,因为数据就在 kernel 里。

server:

[hqinglau@centos 01-015]$ ./a.out 
usage: a.out ip_address port_number filename
[hqinglau@centos 01-015]$ ./a.out localhost 12347 00_model.cc

client:

[hqinglau@centos ~]$ telnet localhost 12347
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
#include <sys/socket.h>
#include <netinet/in.h>

    ...
    close(connfd);
    close(sock);

    return EXIT_SUCCESS;
}
Connection closed by foreign host.

writev

**readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。**有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

两个函数的返回值:若成功则返回已读、写的字节数,若出错则返回-1。

struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};

这里有一个问题,数据太大可能会读不完或者写不完,下一次读写的时候就有更改iov数组元素的起始地址和长度。这个在tinyWebServer的项目中再具体谈。