通过TCP协议进行C/S模式的网络通信

学习要由浅入深、由易到难,分析Linux内核中网络部分就要从内核对外提供的socket封装接口说起,典型以TCP协议C/S方式socket通信大致过程如图所示:


(图片来源于网络)

从图中可以看到TCP服务端server的初始化过程复杂一些,就像开一个小卖铺,你要登记为个体工商户其中最重要的就是营业地址(也就是bind绑定IP地址和端口号),然后就可以开门营业了(listen),营业需要有营业员在那等着接待客户(也就是accept),这样就完成了TCP服务端server的初始化。

TCP客户端client的初始化比较简单一些,就像你要去小卖铺买东西,你只要知道小卖铺的营业地址(IP地址和端口号),就可以去买东西了(connect)。

客户端connect服务端accept对接上了,客户和营业员就可以谈生意,你一句我一句(send和recv),达成交易客户端close离场,服务端继续等着接待客户(也就是accept)。

服务端代码

接下来以一个简单代码hello/hi范例来具体了解TCP协议C/S方式socket通信代码。
首先看服务端程序代码,来一个客户就reply hi。

#include"syswrapper.h" #define MAX_CONNECT_QUEUE 1024 int main() { char szBuf[MAX_BUF_LEN] = "\0"; char szReplyMsg[MAX_BUF_LEN] = "hi\0"; InitializeService(); while(1) { ServiceStart(); RecvMsg(szBuf); SendMsg(szReplyMsg); ServiceStop(); } ShutdownService(); return 0; }

客户端代码

然后看客户端程序代码,发送hello,接收hi。

#include"syswrapper.h" #define MAX_CONNECT_QUEUE 1024 int main() { char szBuf[MAX_BUF_LEN] = "\0"; char szMsg[MAX_BUF_LEN] = "hello\0"; OpenRemoteService(); SendMsg(szMsg); RecvMsg(szBuf); CloseRemoteService(); return 0; }

socket接口封装代码

以上客户端和服务端代码我们都做了简单的封装,实际上看不到具体的socket代码,具体用到socket接口的代码如下:

/********************************************************************/ /* Copyright (C) SSE-USTC, 2012 */ /* */ /* FILE NAME : syswraper.h */ /* PRINCIPAL AUTHOR : Mengning */ /* SUBSYSTEM NAME : system */ /* MODULE NAME : syswraper */ /* LANGUAGE : C */ /* TARGET ENVIRONMENT : Linux */ /* DATE OF FIRST RELEASE : 2012/11/22 */ /* DESCRIPTION : the interface to Linux system(socket) */ /********************************************************************/ /* * Revision log: * * Created by Mengning,2012/11/22 * */ #ifndef _SYS_WRAPER_H_ #define _SYS_WRAPER_H_ #include<stdio.h> #include<arpa/inet.h> /* internet socket */ #include<string.h> //#define NDEBUG #include<assert.h> #define PORT 5001 #define IP_ADDR "127.0.0.1" #define MAX_BUF_LEN 1024 /* private macro */ #define PrepareSocket(addr,port) \ int sockfd = -1; \ struct sockaddr_in serveraddr; \ struct sockaddr_in clientaddr; \ socklen_t addr_len = sizeof(struct sockaddr); \ serveraddr.sin_family = AF_INET; \ serveraddr.sin_port = htons(port); \ serveraddr.sin_addr.s_addr = inet_addr(addr); \ memset(&serveraddr.sin_zero, 0, 8); \ sockfd = socket(PF_INET,SOCK_STREAM,0); #define InitServer() \ int ret = bind( sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Bind Error,%s:%d\n", \ __FILE__,__LINE__); \ close(sockfd); \ return -1; \ } \ listen(sockfd,MAX_CONNECT_QUEUE); #define InitClient() \ int ret = connect(sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Connect Error,%s:%d\n", \ __FILE__,__LINE__); \ return -1; \ } /* public macro */ #define InitializeService() \ PrepareSocket(IP_ADDR,PORT); \ InitServer(); #define ShutdownService() \ close(sockfd); #define OpenRemoteService() \ PrepareSocket(IP_ADDR,PORT); \ InitClient(); \ int newfd = sockfd; #define CloseRemoteService() \ close(sockfd); #define ServiceStart() \ int newfd = accept( sockfd, \ (struct sockaddr *)&clientaddr, \ &addr_len); \ if(newfd == -1) \ { \ fprintf(stderr,"Accept Error,%s:%d\n", \ __FILE__,__LINE__); \ } #define ServiceStop() \ close(newfd); #define RecvMsg(buf) \ ret = recv(newfd,buf,MAX_BUF_LEN,0); \ if(ret > 0) \ { \ printf("recv \"%s\" from %s:%d\n", \ buf, \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ } #define SendMsg(buf) \ ret = send(newfd,buf,strlen(buf),0); \ if(ret > 0) \ { \ printf("rely \"hi\" to %s:%d\n", \ (char*)inet_ntoa(clientaddr.sin_addr), \ ntohs(clientaddr.sin_port)); \ } #endif /* _SYS_WRAPER_H_ */

这里通过宏定义的方式对socket接口做了简单的封装,封装起来有两个好处:一是把所有和socket有关的代码放在一起便于维护和移植,另一个是使得上层代码的业务过程更清晰。当然这里与我们理解socket接口的关系不太大,能理解socket的通信过程就好。

这段代码里涉及了socket接口的相关内容,比如网络地址的结构体变量、socket函数及其参数等,需要我们仔细研究了解他们的具体作用。

sockaddr和sockaddr_in的不同作用

一般在linux环境下/usr/include/bits/socket.h或/usr/include/sys/socket.h可以看到sockaddr的结构体声明。

/* Structure describing a generic socket address. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ };

这是一个通用的socket地址可以兼容不同的协议,当然包括基于TCP/IP的互联网协议,为了方便起见互联网socket地址的结构提供定义的更具体见/usr/include/netinet/in.h文件中的struct 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)]; };

sockaddr和sockaddr_in的关系有点像面向对象编程中的父类和子类,子类重新定义了父类的地址数据格式。同一块数据我们根据需要使用两个不同的结构体变量来存取数据内容,这也是最简单的面向对象编程中的多态特性的实现方法。

AF_INET和PF_INET

在/usr/include/bits/socket.h或/usr/include/sys/socket.h中一般可以找到AF_INET和PF_INET的宏定义如下。

/* Protocol families. */ ... #define PF_INET 2 /* IP protocol family. */ ... /* Address families. */ ... #define AF_INET PF_INET ...

尽管他们的值相同,但它们的含义是不同的,网上很多代码将AF_INET和PF_INET混用,如果您了解他们的含义就不会随便混用了,根据如下注释可以看到A代表Address families,P代表Protocol families,也就是说当表示地址时用AF_INET,表示协议时用PF_INET。参见我们实验室代码中的使用方法,“serveraddr.sin_family = AF_INET;”中使用AF_INET,而“sockfd = socket(PF_INET,SOCK_STREAM,0);”中使用PF_INET。

SOCK_STREAM及其他协议

在/usr/include/bits/socket_type.h可以找到“__socket_type”,不同协议族一般都会定义不同的类型的通信方式,对于基于TCP/IP的互联网协议族(即PF_INET),面向连接的TCP协议的socket类型即为SOCK_STREAM,无连接的UDP协议即为SOCK_DGRAM,而SOCK_RAW 工作在网络层。SOCK_RAW 可以处理ICMP、IGMP等网络报文、特殊的IPv4报文等。

/* Types of sockets. */ enum __socket_type { SOCK_STREAM = 1, /* Sequenced, reliable, connection-based byte streams. */ #define SOCK_STREAM SOCK_STREAM SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams of fixed maximum length. */ #define SOCK_DGRAM SOCK_DGRAM SOCK_RAW = 3, /* Raw protocol interface. */ #define SOCK_RAW SOCK_RAW SOCK_RDM = 4, /* Reliably-delivered messages. */ #define SOCK_RDM SOCK_RDM SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based, datagrams of fixed maximum length. */ ... 

如上几点对于我们后续进一步理解和分析Linux网络代码比较重要,代码中涉及的其他接口及参数可以在实验过程中自行查阅相关资料。

实验指导

本实验环境见 https://www.shiyanlou.com/courses/1198#labs
以上代码可以clone linuxnet.git并参照如下指令编译执行代码:

shiyanlou:~/ $ cd cd LinuxKernel shiyanlou:Code/ $ git clone shiyanlou:Code/ $ cd linuxnet shiyanlou:linuxnet/ (master) $ cd lab1 shiyanlou:lab1/ (master) $ ls client.c server.c syswrapper.h shiyanlou:lab1/ (master) $ make shiyanlou:lab1/ (master*) $ ./server recv "hello" from 127.0.0.1:58911 send "hi" to 127.0.0.1:58911

右击水平分割Xfce终端(Terminal),执行client

shiyanlou:lab1/ (master*) $ ./client send "hi" to 0.0.0.0:60702 recv "hi" from 0.0.0.0:60702 shiyanlou:lab1/ (master*) $ 

本博文摘取自专栏《庖丁解牛Linux网络核心》,现在订阅,抢200个早鸟名额!

专栏说明

首先声明本专栏的目标并不是帮助大家获得立即可能使用的专业技能,而是希望能通过研究分析Linux内核中网络部分的代码实现来深刻理解互联网运作的核心机制,看完本专栏预期可以达成如下目标:

从整体上理解互联网运作的方式;
能分析上网打开一个网页的过程中互联网底层具体做了哪些工作,从而在遇到网络相关问题时能独立分析定位问题;
由于我们涉及的实验都是在Linux系统完成的,您还会进一步熟悉Linux系统;
分析Linux内核中网络部分当然也少不了对网络协议及RFC文档的讨论,相信您也能对网络标准有更多的了解。