庖丁解牛Linux網絡核心
學習要由淺入深、由易到難,分析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/teacher/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文檔的討論,相信您也能對網絡標準有更多的了解。
庖丁解牛Linux網絡核心