1. 程式人生 > >技術系列之 網路模型(一)基礎篇

技術系列之 網路模型(一)基礎篇

全文針對linux環境。tcp/udp兩種server種,tcp相對較複雜也相對比較常用。本文就從tcp server開始講起。先從基本說起,看一個單執行緒的網路模型,處理流程如下:

socket-->bind-->listen-->[accept-->read-->write-->close]-->close

[]中程式碼迴圈執行,[]外的是對監聽socket的處理,[]內的是對accept返回的客戶socket的處理。這些系統呼叫的引數以及需要的標頭檔案等,只需要在linux下man就好。

一、注意事項。
(1)包裹巨集使用。這些系統呼叫返回-1表示失敗。檢測系統呼叫的返回值是個好習慣,應該說必須檢測,如果系統呼叫總是成功的話,它為何又要有返回值呢?。每次檢查的話,程式碼寫起來又很是羅唆,並且容易遺漏檢測。使用巨集包裹系統呼叫或者使用包裹函式是不錯的方案。下面給出幾個預定義包裹巨集:

#define NOERROR_FUNC(func,opt) if((func)<0) /
 { /
  
printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /
  opt; 
/
 }
#define NOERROR_FUNC_1(func) NOERROR_FUNC(func,return -1)
#define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,return NULL)

不知道strerror?,剛說了,去linux下:man strerror
以後使用就可以類似於這樣:

NOERROR_FUNC_1((fd
=socket(AF_INET,SOCKET_STREAM,0)));
NOERROR_FUNC_1(bind(fd,(struct sockaddr 
*)&serverAddr,sizeof(struct sockaddr_in)));


(2)不能返回失敗的錯誤。大多數阻塞式系統呼叫要處理EINTR錯誤,另accept還要處理ECONNABORTED。與(1)同樣道理,預定義巨集如下:

#define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) if((func)<0) /
 { /
  
printf("Line[%d] error[%d:%s]/n",
__LINE__,errno,strerror(errno)); /
  
if(errno==err) { erropt;} /
  
else {opt;} /
 }
#define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) if((func)<0) /
 { /
  
printf("Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /
  
if(errno==err1||errno==err2) { erropt;} /
  
else {opt;} /
 }

呼叫accept的程式碼就可以如此寫:

while(1)
 
{
  client_sockfd
=accept(fd,(struct sockaddr *)&clientAddr,&lenAddr);
  NOERROR_FUNC_BUT_ERR_2(client_sockfd,retun 
-1,EINTR,ECONNABORTED,continue);
(3)涉及到系統呼叫分兩類:從使用者態到核心態,該類系統呼叫使用值引數,有:bind/setsockopt/connect;從核心態到使用者態,該類系統呼叫使用值-結果引數,有:accept/getsockopt。
看下兩者函式原型,從使用者態到核心態: int setsockopt(int s, int level, int optname, constvoid*optval, socklen_t optlen);
       
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
       
int bind(int sockfd,struct sockaddr *Addr,socklen_t addrlen);

從核心態到使用者態:

int getsockopt(int s, int level, int optname, void*optval, socklen_t *optlen);
    
int accept(int sockfd,struct sockaddr *Addr,socklen_t *addrlen);

看最後一個引數,從使用者態到核心態只要告訴核心引數長度的值就可以了,因此是值方式。從核心態到使用者態,要事先準備好變數儲存核心態返回的結果長度值,因此是指標方式,稱之為值-結果引數。

二、系統呼叫
(1)socket建立一個ipv4的tcp socket
(2)bind
把socket繫結到一個地址,首先要指明地址,如下:

int fd;
   NOERROR_FUNC_1(fd=socket(AF_INET,SOCKET_STREAM,0)); struct sockaddr_in addr;
addr.sin_family
=AF_INET;//協議型別
addr.sin_port=htons(5000);//埠地址
addr.sin_addr.s_addr=htonl(INADDR_ANY);//此處表示任意ip(主機有多個網絡卡,則將環路地址127.0.0.1以及各網絡卡ip都指定)。
NOERROR_FUNC_1(bind(fd,(struct sockaddr *)addr,sizeof(struct sockaddr_in)));

建立ipv4協議的地址,使用5000埠,接收任何地址的connect,把該地址和fd繫結。
注意:
1、地址宣告的時候使用struct sockaddr_in,使用的時候總是強制轉化為struct sockaddr。
2、struct sockaddr_in結構中埠和ip都必須是網路序。htons把主機序的short int轉化為網路序,htonl把主機序的long int轉化為網路序。
3、除任意ip地址為常量外,一般習慣用點分字串表示ip地址,而addr.sin_addr.s_addr要使用網路序整型。
因此有兩個函式可以在字串和網路序ip地址之間做轉換:

constchar*inet_ntop(int af, constvoid*src,char*dst, socklen_t cnt);
   
int inet_pton(int af, constchar*src, void*dst);

這裡是需要網路序,因此使用ton(to net)那個函式,比如:

NOERROR_FUNC_1(inet_pton(AF_INET,"172.168.0.45"&addr.sin_addr.s_addr));

(3)setsockopt

long val;
socklen_t len
=sizeof(val);
NOERROR_FUNC_1(setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,
&(val=1),len));

給socket設定選項,常用的不多,SO_REUSEADDR是一個,伺服器一般使用,其它還有SO_RCVBUF,SO_SNDBUF。accept返回的對端socket繼承監聽socket的傳送快取、接收快取選項。一般也不需要設定SO_RCVBUF,SO_SNDBUF,預設的足夠了,頻寬很大的情況下,需要設定,以免其稱為瓶頸,貌似預設的是8092位元組。哦,還有要在listen前設定。
(4)listen把fd從主動埠變為被動埠,等待client connect。第二個引數是表示三次握手中佇列以及完成了三次握手等待accept系統函式來取的佇列的相加值,有的系統不是簡單相加,還有一個係數,也就是如果設定5,係數是2,那麼兩個佇列的和就是10。如果佇列滿,而accept沒來取(很忙的情況下,來不及呼叫accept),再有連線來就會被拒絕掉,要想系統能處理超大爆發的連線,就加大這個引數值,加快accept的處理。SOMAXCONN表示取系統允許的最大值。
(5)accept
前面已經舉例了,這裡就不再列例子了。
阻塞式呼叫,需要處理EINTR(被訊號終止),ECONNABORTED(返回前client異常終止),處理的方式就是重新accept。
(6)read這是針對檔案描述符的一個系統呼叫,socket也屬於檔案描述符。tcp協議中傳輸的資料都是流位元組,沒有什麼結束符的標誌,只能由協議提供結束方式,比如http協議使用"/r/n/r/n"或者"/n/n"標識一條信令結束,這樣的話,我們只能一個位元組一個位元組的讀取,然後結合已經讀取的位元組,判斷是否應該結束讀。而網路模型中要提高效能,一個重要方面就是要減少系統呼叫的次數。因此tcp中都要使用快取區一次讀取儘可能多的資料,然後再從該快取區一個位元組一個位元組的讀取,快取區資料被讀完而沒有到結束位置的時候,再次呼叫系統呼叫read。
返回值為0表示對端正常關閉,大於0表示讀取到的位元組數。示例見最後例子。
(7)write兩個需要注意的地方:
1、對EINTR處理。防止被訊號中斷,沒有正確寫入需求的字元數。
2、signal(SIGPIPE, SIG_IGN);這句程式碼的意思是忽略SIGPIPE訊號。
write寫被重置(對端意外關閉)的套介面,產生SIGPIPE訊號,不處理的話程式被終止。忽略的話,繼續寫會產生EPIPE錯誤,檢查write系統呼叫的返回結果就好了。示例見最後例子。
signal的使用,man下就看到了,回撥函式的原型等都有,SIG_IGN也會出現,呵呵。
(8)close就不說了
(9)fcntl

要對socket設定為非阻塞方式,setsockopt沒有提供相應的選項,只能用fcntl函式設定。

NOERROR_FUNC_1(listen(fd,SOMAXCONN)); int read(int fd,char*buf,size_t len); int write(int fd,char*buf,size_t len); int flags;
NOERROR_FUNC_1(flags
=fcntl(client_sockfd,F_GETFL,0));
NOERROR_FUNC_1(fcntl(client_sockfd,F_SETFL,flags
|O_NONBLOCK));

多路分離I/O(select/poll/epoll)通常設定為非阻塞方式。
設定為阻塞方式(預設方式)程式碼:

int flags;
NOERROR_FUNC_1(flags
=fcntl(client_sockfd,F_GETFL,0));
NOERROR_FUNC_1(fcntl(client_sockfd,F_SETFL,flags
&~O_NONBLOCK));

對於阻塞方式的套介面,如果要避免read write永遠阻塞,設定等待時間的方式有3種:訊號方式,不推薦,不說了;select方式,每次呼叫read前呼叫select監視該套介面是否在指定時間內可寫,超時select返回0,這樣每次執行read都要呼叫兩個系統呼叫,不推薦;最後就是設定套介面選項SO_RECVTIMEO和SO_SNDTIMEO,其實這個也不推薦,總之不推薦阻塞式的方式,呵呵。實用的網路模型都是多路分離的。
非阻塞方式下的connect函式要說下,當然是就客戶端而言,connect後如果沒有立即返回連線成功的話,把這個socket加入select的 fd_set(poll的pollfd,epoll的EPOLL_CTL_ADD操作),要監視是否可寫事件,可寫的時候用getsockopt獲取SO_ERROR選項,如果非負(其實就是0值)就標示connect成功,否則就是失敗。EPOLL中測試結果是connect失敗的返回事件是EPOLLERR|EPOLLHUP,並不是加入時的EPOLLOUT,成功的時候是EPOLLOUT。

三、示例
最後給個單執行緒的伺服器,雖說沒什麼實用意義,不過就象“hello world!”,入門第一課。
這個例子,讀取資料,回寫response,關閉clientfd。不管read write是否出錯,都執行close,因此程式碼很簡單。
先來main函式:

int main()
{
    
int server_sockfd;
    
int client_sockfd;
    struct sockaddr_in serverAddr;
    struct sockaddr_in clientAddr;
    size_t lenAddr;
        int val;

    memset(
&serverAddr,0,sizeof(serverAddr));
    serverAddr.sin_family
=AF_INET;
    serverAddr.sin_port
=htons(5000);
    serverAddr.sin_addr.s_addr
=htonl(INADDR_ANY);

    NOERROR_FUNC_1((server_sockfd
=socket(AF_INET,SOCK_STREAM,0)));
    NOERROR_FUNC_1(setsockopt(server_sockfd,SOL_SOCKET,SO_REUSEADDR,
&(val=1),sizeof(val)));
    NOERROR_FUNC_1(bind(server_sockfd,(struct sockaddr 
*)&serverAddr,sizeof(struct sockaddr_in)));
    NOERROR_FUNC_1(listen(server_sockfd,SOMAXCONN));
 
 
    
conststaticchar* response="HTTP/1.1 200 OK/r/n/r/n";
    
char buf[BUF_LEN];
    signal(SIGPIPE, SIG_IGN);
    
while(1)
    
{
        client_sockfd
=accept(server_sockfd,(struct sockaddr *)&clientAddr,&lenAddr);
        NOERROR_FUNC_BUT_ERR_2(client_sockfd,
return-1,EINTR,ECONNABORTED,continue);
        BuffCache cache;
        
if(read_double_enter(client_sockfd,buf,BUF_LEN,&cache)>0)
            writen(client_sockfd,response,
19);
        close(client_sockfd);
    }

    close(server_sockfd);
    
return0;
}

 下面是包含的標頭檔案和巨集:

#include <unistd.h>
#include 
<sys/types.h>
#include 
<sys/socket.h>
#include 
<arpa/inet.h>
#include 
<stdio.h>
#include 
<errno.h>
#include 
<signal.h>
#include 
<stdlib.h>
#include 
<string.h>
#include 
<stdarg.h>


#define NOERROR_FUNC(func,opt) 
if((func)<0) /
    
{ /
        printf(
"Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /
        opt; /
    }

#define NOERROR_FUNC_BUT_ERR(func,opt,err,erropt) 
if((func)<0) /
    
{ /
        printf(
"Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /
        
if(errno==err) { erropt;} /
        
else{opt;} /
    }

#define NOERROR_FUNC_BUT_ERR_2(func,opt,err1,err2,erropt) 
if((func)<0) /
    
{ /
        printf(
"Line[%d] error[%d:%s]/n",__LINE__,errno,strerror(errno)); /
        
if(errno==err1||errno==err2) { erropt;} /
        
else{opt;} /
    }


#define NOERROR_FUNC_1(func) NOERROR_FUNC(func,
return-1)
#define NOERROR_FUNC_NULL(func) NOERROR_FUNC(func,
return NULL)

#define BUF_LEN 
1024


下面是快取區和讀寫程式碼:

class BuffCache
{
public:
    BuffCache():count(
0){}
    
int read_socket(int fd,char* pCh)
    
{
        
if(count<=0)
        
{
        again:
            
if((count=read(fd,buf,BUF_LEN))<0)
            
{
                
if(errno==EINTR)
                    
goto again;
                
*pCh='/0';
                
return-1;
            }

            
elseif(count==0)
            
{
                
*pCh='/0';
                
return0;
            }

            ptrBuf
=buf;
        }

        count
--;
        
*pCh=*(ptrBuf++);
        
return1;
    }

private:
    
char buf[BUF_LEN];

相關推薦

技術系列 網路模型基礎

全文針對linux環境。tcp/udp兩種server種,tcp相對較複雜也相對比較常用。本文就從tcp server開始講起。先從基本說起,看一個單執行緒的網路模型,處理流程如下: socket-->bind-->listen-->[accept-->

技術系列 網路模型

作者:CppExplore 網址:http://www.cppblog.com/CppExplore/本章主要列舉伺服器程式的各種網路模型,示例程式以及效能對比後面再寫。一、分類依據。伺服器的網路模型分類主要依據以下幾點(1)是否阻塞方式處理請求,是否多路複用,使用哪種多路複

【原創】技術系列 執行緒

作者:CppExplore 網址:http://www.cppblog.com/CppExplore/廢話不多說,詳細介紹使用執行緒的優點好處請參考baidu、google。一、執行緒使用場景。使用執行緒的方式大致有兩種:(1)流水線方式。根據業務特點,將一個流程的處理分割成多個執行緒,形成流水線的處

【原創】技術系列 狀態機

作者:CppExplore 網址:http://www.cppblog.com/CppExplore/一、狀態機描述狀態機理論最初的發展在數位電路設計領域。在數位電路方面,根據輸出是否與輸入訊號有關,狀態機可以劃分為Mealy型和Moore型狀態機;根據輸出是否與輸入訊號同步,狀態機可以劃分為非同步和

技術系列 定時器

一、 基礎知識1、時間型別。Linux下常用的時間型別有4個:time_t,struct timeval,struct timespec,struct tm。(1)time_t是一個長整型,一般用來表示用1970年以來的秒數。(2)Struct timeval有兩個成員,一個

入門到放棄node系列網路模組

前言 本文首發公眾號【一名打字員】 上一次相信大家都基本瞭解node的用法了,有做功課的童鞋肯定回去溫習了一下js的語法。這些年來js發展很快,出了很多類似許多vue、react、node等等眾所周知的玩意兒,對應的社群配套也越來越完善。好的,接下來我們補充

反向教學系列——PHP入門

water oui 版本 名稱 令行 完全 技術 地址 安裝目錄 php是什麽?其實就是html的功能加強版。網頁本來在服務器上,如果客戶端問服務器索取網頁文件(xxx.html),那麽服務器就會把客戶端指定的網頁發回去。(根據我的理解,)php是因“表單”而誕生的,所謂表

反向教學系列——Django入門【不需知道web框架】

Django 教程 反向教學 一派胡言 用這東西最終是建網站的,或者是更一般意義的服務器。服務器麽,就是如果用別的電腦(“客戶機”)給它發請求,它會返回一些東西——如果給隨便某個機器發信息,它自然未必理你。要想某機器回應你,得滿足這些條件——它不處在關機狀態它能收到你的信息,你也能收到它的信息

C/S+P2P網路模型--聊天

原文地址 從今天開始我們來實現一個C/S+P2P網路模型,主要功能包括:聊天和傳輸檔案。 聊天分群聊和私聊,雖然不是真的像QQ那樣有個QQ群,但是這樣類比容易理解,在後面我們會仔細說明。 前面我寫過一篇文章講過組播,我們就用組播來實現p2p網路模型。首先而且是最關鍵的一點是:每一個程式例

【原創】技術系列 記憶體管理

作者:CppExplore 地址:http://www.cppblog.com/CppExplore/(2)boost::pool系列。boost的記憶體池最低層是simple_segregated_storage,類似於Loki中的chunk,在其中申請釋放block(boost中把block稱為c

【原創】技術系列 狀態機

與常規狀態機相比,它的FSM_STATE結構沒有default_func,多了 FSM_STATE_ID parent; FSM_STATE_ID default_child;兩個結構。狀態機初始化的時候可以指定預設狀態,為了防止指定的狀態非葉結點,增加fsm_init方法。該狀態機的事件處理演算

細說網路那些事兒網路基本功:細說網路傳輸

網路基本功(一):細說網路傳輸 常言道:欲練神功,必先練好基本功。之前做了一個關於IP路由,預設閘道器和掩碼的問答貼,做完這個帖子覺得如果對網路知識點做一個系統的闡述,應該會很有幫助。 本系列文章著重於講解網路管理實際應用中常常涉及的重要知識點,儘量以實用為主。準備

技術系列 記憶體管理

2、定長記憶體池。典型的實現有LOKI、BOOST。特點是為不同型別的資料結構分別建立記憶體池,需要記憶體的時候從相應的記憶體池中申請記憶體,優點是可以在使用完畢立即把記憶體歸還池中,可以更為細粒度的控制記憶體塊。    與變長的相比,這種型別的記憶體池更加通用,另一方面對於

技術系列 記憶體管理

(2)boost::pool系列。boost的記憶體池最低層是simple_segregated_storage,類似於Loki中的chunk,在其中申請釋放block(boost中把block稱為chunk,暈死,這裡還是稱其為block)採用了和loki的chunk中同樣

計算機網路複習總結網路

最近也準備臨近考試月了,抽時間精簡總結一下《計算機網路》。這是一篇關於計算機網路的第三層,網路層相關的知識。 在計算機網路的分層中,網路層的作用就是將分組從源主機沿網路路徑發到目的主機上,所以網路層裡最核心的功能就是:分組轉發 和 路由選擇。 分組轉發和路由

C語言網路程式設計域名解析

在網路程式設計時,知道域名是不能直接訪問一個主機的,需要轉換成相應的IP地址。有時在程式中需要將一個IP地址轉換成一個域名。本節將講解C程式中的IP地址與域名的轉換問題。 提示:在TCP/IP網路中,通訊雙方的主機必須知道彼此的IP地址方可進行正常的通訊,如果給出的主機的域

OJ 系列常規練習題

1、求M的n次方最後三位 【問題描述】 正整數M 的N次方有可能是一個非常大的數字,我們只求該數字的最後三位 例1:比如輸入5和3 ,5的3次方為125,則輸出為125 例2:比如輸入2和10 2的10次方為1024 ,則輸出結果為24 例3:比如

技術系列 執行緒

為了後面寫的《網路模型(二)》,多寫一篇關於執行緒的。執行緒使用涉及的主要資料結構以及應用框架可以參考http://www.cppblog.com/CppExplore/archive/2008/01/15/41175.html。本文的主要目的是給出linux下實用的執行緒訊

springboot原始碼解析-管中窺豹系列總體結構

# 一、簡介 - Springboot原始碼解析是一件大工程,逐行逐句的去研究程式碼,會很枯燥,也不容易堅持下去。 - 我們不追求大而全,而是試著每次去研究一個小知識點,最終聚沙成塔,這就是我們的springboot原始碼管中窺豹系列。 ![ 簡介 ](https://zhangbin1989.gitee.

網路管理基礎知識

1、使用ipconfig命令檢視計算機中所有介面卡的TCP/IP配置資訊     ipconfig命令的作用是顯示所有當前的TCP/IP網路配置值、重新整理動態主機配置協議情況(DHCP)和域名設定(DNS)。     引數說明如下: