庖丁解牛Linux網路核心
通過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/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文件的討論,相信您也能對網路標準有更多的瞭解。