功能
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_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;
}
复制代码
至此,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主动关闭端口退出。
我们常听说三次握手,四次挥手,可是就剩两个包了啊。
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的项目中再具体谈。
目前文章:
【一】tinyWebServer实战:socket讲解+抓包分析
欢迎关注公众号【高性能linux后台开发】,学习更多内容。