網絡編程總結(一)

  這幾天在看muduo網絡庫,順便第二次詳細的精讀一下《unix網絡編程》。
  在這裏從最基礎的編程模型開始,記錄一下一步步改進程序的過程和細碎的知識點。
  首先看一下啓動一個服務器程序所必須的庫函數。
  
基本TCP客戶/服務器程序的套接字函數

  • socket
#include <sys/socket.h>
int socket(int family,int type,int protocol);
  1. family參數指明協議族,AF_INET(ipv4),AF_INET(ipv6)等。
  2. type參數指明套接字類型,TCP是字節流協議,僅支持sock_stream。
  3. protocol參數指明傳輸協議,共有TCP,UDP,SCTP三類。
  4. AF_XXX與PF_XXX通常沒有區別。

  • connect
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr, socklen_t addrlen);
                        返回:若成功返回0,出錯返回-1
  1. sockfd是socket函數返回的套接字描述符,二參數爲指向套接字地址結構的指針,三爲該結構大小。
  2. 在connect函數中將激發TCP三次握手。爲什麼TCP需要三次握手?而不是兩次或者更多,首先因爲理論上,三次是確保可靠的基本次數,而不是保證能可靠的次數。其次它能防止無效的報文請求連接,比方一段請求的報文因爲網絡延遲一直被滯留,等到連接釋放後纔到達server,這時候server會發確認,client並不理睬,server會一直阻塞等待client的數據,server的資源就被白白浪費了。
  3. 函數的出錯返回有幾種情況,大致上分爲timeout,這時候一般會有重傳機制;服務器響應RST,指定的端口上沒有進程等待連接;路由器上的destination unreachable。

  • bind
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
  1. 主要有兩種用法,一是服務器啓動的時候綁定一個發佈給大家的端口。
  2. 二是把特定IP地址綁定到它的套接字上。
  3. bind可以指定IP和端口號,也可以不指定。對於IPV4,常量值INADDR_ANY是通配地址,一般爲0,由內核選擇IP。

  • listen
#include <sys/socket.h>
int listen(int sockfd,int backlog);
  1. 函數應該在socket和bind之後調用,其實剛開始學習的同學可能對bind、connect和listen不太理解,bind是實現套接字和協議族的捆綁,而listen負責被動的偵聽指向該套接字的連接,是從服務器端的被動行爲,connect是屬於客戶端主動發起的行爲。
  2. sockfd就是被偵聽的套接字啦,backlog指定的是排隊的最大連接個數,監聽套接字一般有兩個隊列。未完成連接隊列,就是三次握手尚未成功的連接,已完成隊列就是已經握手完,處於連接狀態。

  • accept
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
                    返回:成功返回非負描述符,出錯返回-1
  1. 剛纔listen函數提到兩個隊列,未完成連接和已完成連接隊列,accept就是從已完成連接隊列的隊頭返回一個連接,隊列爲空的時候,進程休眠。
  2. 注意!!!,這裏有兩個描述符,一個是作爲第一個參數的監聽描述符,另一個是作爲返回值的由內核生成的全新的描述符。一個服務器通常僅僅創建一個監聽套接字,在其聲明週期一直存在。然後,內核爲每一個客戶創建一個已連接套接字,完成服務,此套接字關閉。

  這裏給出一個返回服務器時間的小例子,我的代碼是運行在cloud9平臺上的,非常好的一個在線集成環境,我選的是C++編程環境。它會自動生成一個makefile文件,簡單定義makefile就是幫助你完成編譯的腳本文件,你也可以選擇在命令行一個一個輸入編譯命令。文件大致如下:
  

all: hello-cpp-world hello-c-world

%: %.cc
    g++ -std=c++11 $< -o $@

%: %.c
    gcc $< -o $@

  下面是服務器代碼,大致流程跟開始的圖片流程一樣:
  

#include <iostream>
#include "unp.h"
void errorHandling(const char *message);  // 錯誤處理
int main() {
    int listnenfd,connfd;
    struct sockaddr_in servaddr;
    //用來存儲時間字符串的緩衝區
    char buff[MAXLINE];
    //測試客戶端返回數據用的緩衝區
    char readBuf[MAXLINE];
    time_t ticks;
    //建立套接字
    listnenfd = socket(PF_INET,SOCK_STREAM,0);

    //代替memset進行初始化的函數
    bzero(&servaddr, sizeof(servaddr));
    //設置協議族
    //IPV4
    servaddr.sin_family = AF_INET;
    //這裏出現一個htonl函數,是將host主機字節序轉換成network網絡字節序
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //監聽的端口
    servaddr.sin_port = htons(8081);
    //綁定端口
    if(-1== bind(listnenfd,(SA*)&servaddr, sizeof(servaddr)))
        errorHandling("socket_error");
    //開始監聽
    if(-1==listen(listnenfd,LISTENQ))
        errorHandling("listen() error");

    //std::cout<<"now listen"<<std::endl;
    //現在服務器屬於迭代服務器,一次只能服務一個連接,這當然是不太有效率的
    for(;;){
        connfd = accept(listnenfd, (SA*)NULL, NULL);
        //read(connfd,readBuf,sizeof(readBuf)-1);
        //printf("%s",readBuf);
        //獲取時間寫入緩衝區後發送出去
        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        //以後可以用sendmsg等代替
        write(connfd, buff, strlen(buff));

        close(connfd);
    }
    std::cout<<"test "<<std::endl;
}
void errorHandling(const char *message){
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

用到的頭文件

#ifndef _unp_h
#define _unp_h
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#define SA struct sockaddr

#define MAXLINE 1024
#define LISTENQ 1024
#define CPU_VENDOR_OS "Linux"

#endif

編譯後在後臺運行

g++ server.cc -o server
./server &

客戶端程序:

#include "unp.h"
#include "myerr.h"
int main(int argc,char** argv){
    int sockfd,n;
    char recvline[MAXLINE+1];
    struct sockaddr_in servaddr;

    if(argc != 2)
        err_quit("usage: a.out<IPaddress>");

    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("socket error");

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8081);
    //這裏客戶端帶參數,下面的函數將字符串裝換成二進制結果存放在sin_addr中
    if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
        err_quit("inet_pton error for %s",argv[1]);

    if(connect(sockfd,(SA *)&servaddr, sizeof(servaddr)) <0 )
        err_sys("connect error");
    //讀取到服務器發送的字節後輸出到標準輸出
    while((n = read(sockfd, recvline, MAXLINE)) >0 ){
        recvline[n] = 0;
        if(fputs(recvline, stdout) == EOF)
            err_sys("fputs error");
    }
    if (n < 0)
        err_sys("read error");

    exit(0);
}

編譯並運行程序

g++ daytimetcpcli.cc -o client
./client 127.0.0.1
結果:
Tue Apr  5 13:54:12 2016

這裏順便介紹幾個常用指令:

//顯示所有網絡端口信息
netstat -na
//提供網絡接口信息
netstat -ni
//有了接口名字,獲得接口的詳細信息
ifconfig eth0
//找出本地網絡衆多主機,通過上面出現的廣播地址ping
ping -b XXX.XXX.XXX.XXX

上面的服務器是迭代的,如何實現一個併發的服務器,能給多個連接同時提供服務呢?最簡單的模型是阻塞式的fork+exec。

  • fork
      
#include <unistd.h>
pid_t fork(void);
                    返回:在子進程中爲0,在父進程中爲子進程ID
  1. 這個函數調用一次,返回兩次,父進程就是調用進程中返回一次,返回的是子進程的ID號,而在子進程中又返回一次,返回值爲0。父進程轉移控制器給操作系統,操作系統會複製出一個幾乎一模一樣的進程來完成這個操作。
  2. 正因爲如此,父子進程共享很多東西,例如所有父進程打開的描述符。通常,父進程調用accept後fork,然後子進程接着讀寫這個套接字,父進程關閉此套接字。

    下面是一個併發服務器的簡單輪廓:

pid_t pid;
int listenfd,connfd;
listenfd = socket(...);
bind(listenfd,...);
listen(listenfd,LISTENQ);
//爲什麼不用while(1)大概是爲了省略一次判定,但是編譯器可能已經優化過了
for(;;){
    connfd = accept(listenfd,...);
    if(( pid = fork()) == 0){
        close(listenfd);
        doit(connfd);
        close(connfd);
        exit(0);
    }
    close(connfd);
}

這裏有一個小細節,爲什麼在父進程中的close不會直接終止和客戶的連接呢?因爲每個文件和套接字都有一個引用計數,概念和智能指針中的引用計數類似,計數值爲0才真正關閉描述符,當然想發個TCP的FIN分節也是有辦法的,shutdown函數。

期待下一次的改進。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章