这篇文章将对一个简单的socket例程进行剖析。
这里提供的例子就是一个简单的TCP client/server程序,client主动连接server,并从服务器中得到一条欢迎消息:“[server] welcome client!”。程序的流程参考如下:
参考代码如下:
server端:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> int main(void) { int listenfd = 0,connfd = 0; struct sockaddr_in serv_addr; char sendBuff[1025]; listenfd = socket(AF_INET, SOCK_STREAM, 0); memset(&serv_addr, '0', sizeof(serv_addr)); memset(sendBuff, '0', sizeof(sendBuff)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(55555); bind(listenfd, (struct sockaddr*)&serv_addr,sizeof(serv_addr)); printf("server start successfully!\n"); if(listen(listenfd, 10) == -1){ printf("Failed to listen\n"); return -1; } while(1) { connfd = accept(listenfd, (struct sockaddr*)NULL ,NULL); // accept awaiting request strcpy(sendBuff, "[server] welcome client!"); write(connfd, sendBuff, strlen(sendBuff)); close(connfd); sleep(1); } return 0; }
客户端代码:
#include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <arpa/inet.h> int main(void) { int sockfd = 0,n = 0; char recvBuff[1024]; struct sockaddr_in serv_addr; if((sockfd = socket(AF_INET, SOCK_STREAM, 0))< 0) { printf("\n Error : Could not create socket \n"); return 1; } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(55555); serv_addr.sin_addr.s_addr = inet_addr("192.168.1.101"); if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))<0) { printf("\n Error : Connect Failed \n"); return 1; } memset(recvBuff, '0' ,sizeof(recvBuff)); while((n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) > 0) { recvBuff[n] = 0; if(fputs(recvBuff, stdout) == EOF) { printf("\n Error : Fputs error"); } printf("\n"); } if( n < 0) { printf("\n Read Error \n"); } return 0; }
先谈谈服务器端的listen的原理,listen函数的声明如下:
int listen(int sockfd, int backlog);
接下来讨论客户端和服务器端的三次握手连接。
上图描述的情况简述如下:在三次握手之前,服务器端进行了socket,bind和listen函数的调用,在内核中设立一个socket资源,并准备了相关的连接队列,等待客户端连接的到来;之后服务器就阻塞在accept函数上面。当客户端准备好要连接的服务信息(即为struct sockaddr_in结构体指定特定的值),并传给connect来实现和服务器的三次握手,并确立tcp连接(server端accept函数将返回,客户端connect将返回)。
建立tcp连接之后,就是数据的通讯了,在通讯结束后,要关闭连接,由于tcp是全双工的,要关闭读和写,每次操作要确认,所以关闭tcp需要四次数据包的传递。如下图:
原理介绍清楚了,到了进行试验的时候了。这里,我将使用Wireshark工具在mac平台上进行试验,具体如下:
1. 工具的准备。Wireshark可以在官网上下载到。
2. 在mac运行,还需要设置一下权限,否则Wireshark无法访问网卡(参考《No Interfaces Available In Wireshark Mac OS X》)。在控制台运行如下命令:
sudo chmod 644 /dev/bpf*3. 配置Wireshark;在Wireshark的工具栏点击 ,选择相应的网卡(这里走本地网卡,所以选择lo0),如下图:
4. 由于使用xcode进行调试代码,调试信息也是走本地网络的,所以,我要做一些过滤,以便更好地观察试验结果。具体在工具栏下面的Filter下,填写过滤条件:
ip.addr==192.168.1.101
点击开始按钮,就可以对数据包进行监听了。
在这个试验中,我在xcode中设置了几个断点,具体如下:
服务器端:
客户端:
这里约定,服务器代码第36行记作s36, 客户端代码第25行记作c25。
确保WireShark启动的状态下,启动服务器端(这时会阻塞在s34,accept函数处),然后启动客户端。
1)当代码运行到c25行时,wireShark并没有纪录到任何信息。
2)当客户端运行到c33时(执行完connect函数后,客户端将阻塞在read函数上),服务器运行到s36。这样服务器和客户端建立了tcp连接,wireShark纪录到了三次握手的信息,如下:
3)当服务器端运行到s39,服务器端就向客户端发送了“[server] welcome client!”字符串,这时候客户端从c33唤醒,运行到c38(经过read的调用,程序从内核中取出数据,并打印,运行过c38后,客户端又被阻塞在c33行,等待服务器的消息)。这个时候,wireShark纪录到数据的传输,如下图:
4)当服务器端运行过s39,执行完close之后,客户端将从c33唤醒,这个时候的返回值为0,跳出循环到c41;wireShark纪录到结束的四次数据包交换,如下图:
ok,tcp的剖析暂时到这里,接下来我将写点select,poll等相关的nonblock的文章。
参考:
《Unix Networking Programming Volume 1, Third Edition》
https://langui.sh/2010/01/31/no-interfaces-available-in-wireshark-mac-os-x/