学习要由浅入深、由易到难,分析Linux内核中网络部分就要从内核对外提供的socket封装接口提及,典型以TCP协议C/S方式socket通讯大体过程如图所示:linux
(图片来源于网络)git
从图中能够看到TCP服务端server的初始化过程复杂一些,就像开一个小卖铺,你要登记为个体工商户其中最重要的就是营业地址(也就是bind绑定IP地址和端口号),而后就能够开门营业了(listen),营业须要有营业员在那等着接待客户(也就是accept),这样就完成了TCP服务端server的初始化。github
TCP客户端client的初始化比较简单一些,就像你要去小卖铺买东西,你只要知道小卖铺的营业地址(IP地址和端口号),就能够去买东西了(connect)。编程
客户端connect服务端accept对接上了,客户和营业员就能够谈生意,你一句我一句(send和recv),达成交易客户端close离场,服务端继续等着接待客户(也就是accept)。网络
接下来以一个简单代码hello/hi范例来具体了解TCP协议C/S方式socket通讯代码。
首先看服务端程序代码,来一个客户就reply hi。app
#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。less
#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的通讯过程就好。ide
这段代码里涉及了socket接口的相关内容,好比网络地址的结构体变量、socket函数及其参数等,须要咱们仔细研究了解他们的具体做用。函数
通常在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的关系有点像面向对象编程中的父类和子类,子类从新定义了父类的地址数据格式。同一块数据咱们根据须要使用两个不一样的结构体变量来存取数据内容,这也是最简单的面向对象编程中的多态特性的实现方法。
在/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。
在/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文档的讨论,相信您也能对网络标准有更多的了解。