快速學習C語言三: 開發環境, VIM配置, TCP基礎,Linux開發基礎,Socket開發基礎

上次學了一些C開發相關的工具,此次再配置一下VIM,讓開發過程更爽一些。 另外再學一些linux下網絡開發的基礎,好多人學C也是爲了作網絡開發。html

開發環境

首先得有個Linux環境,有時候家裏機器是Windows,裝虛擬機也麻煩,因此還不如30塊錢 買個騰訊雲,用putty遠程練上去寫代碼呢。node

我一直都是putty+VIM在Linux下開發代碼,好幾年了,只要把putty和VIM配置好,其實 開發效率挺高的。python

買好騰訊雲後,裝個Centos,會分配個外網IP,而後買個域名,在DNSPod解析過去,就 能夠用putty遠程登陸了,putty通常作以下設置。linux

  • window\appearance\font setting:consolas 12pt , 設置字體
  • window\translate\charset:utf-8 , 設置字符集
  • window\selection\action of mouse buttons:windows .. , 設置能夠用鼠標選擇文字
  • window\line of scoreback:20000 ,設置可滾屏的長度
  • connection\auto-login username:root, 設置自動登陸的用戶名
  • connection\seconds of keepalive:10, 設置心跳,防止自動斷開

設置完成後把這個會話起個名字,好比叫qcloud,下次用的時候先加載,而後open 就能夠了, 全部設置會保存起來。這樣配置後putty已經很好用了,但咱們還能夠搞成 自動登陸,不須要每次都輸入密碼。git

  • 在Linux下ssh-keygen -t rsa 生成密鑰對
  • 把私鑰id_isa下載到用scp下載到windows並用puttygen加載並從新保存私鑰。
  • 在windows下新建快捷方式輸入D:\soft\putty.exe -i D:\ssh\wawa.ppk -load "qcloud" 其中-i 指定私鑰位置,-load指定會話名稱,

下次雙擊快捷方式就登陸上去了,並且上面的設置都會生效。對了,putty和puttygen 要在官方下載哦。github

VIM配置

首先安裝最新的VIM.編程

wget ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2
./configure --prefix=/usr/local/vim --enable-multibyte --enable-pythoninterp=yes
make
make install

 

修改下~/.bashrc, 加入以下兩句,可讓vim和vi指定成剛安裝的版本vim

alias vim='/usr/local/vim/bin/vim'
alias vi='vim'

 

簡單配置下VIM,就能夠開工了, 打開~/.vimrc,添加以下:windows

" 基本設置
set nocp
set ts=4
set sw=4
set smarttab
set et
set ambiwidth=double
set nu

" 編碼設置 
set encoding=UTF-8
set langmenu=zh_CN.UTF-8
language message zh_CN.UTF-8
set fileencodings=ucs-bom,utf-8,cp936,gb18030,big5,euc-jp,euc-kr,latin1
set fileencoding=utf-8

 

基本每個VIM最少要配置成這樣,包括生產環境,前半拉主要是設置縮進成4個空格, 後半拉是設置編碼,以便打開文件時不會亂碼。api

若是想開發時更爽一些,就得裝插件了,如今裝插件也很簡單,先裝插件管理工具 pathogen.vim, 以下

mkdir -p ~/.vim/autoload ~/.vim/bundle && curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim

 

而後安裝一個文件模版插件,一個代碼片斷插件,一個智能體彷佛插件就能夠了,傻瓜式 的,以下

# 安裝vim-template
cd ~/.vim/bundle
git clone git://github.com/aperezdc/vim-template.git

# 安裝snipmate
git clone git://github.com/msanders/snipmate.vim.git
cd snipmate.vim
cp -R * ~/.vim

# 安裝clang_complete
yum install clang
git clone https://github.com/Rip-Rip/clang_complete.git
cd clang_complete/
make install

 

再在~/.vimrc里加入以下兩句

execute pathogen#infect()
syntax on
filetype plugin indent on

 

別的插件能不裝就不裝了吧,用的時候再說,如今你打開一個新的.c文件,會自動從模版 里加載一個代碼框架進來,而後輸入main,for,pr等按tab鍵就會自動生成代碼片斷, 而後include頭文件後,裏面的函數,類型等在輸入時按ctrl+n就會自動提示,結構的 成員也能夠,已經很爽了。

TCP基礎

TCP使用很普遍,先了解一下概念,TCP是面向鏈接的協議,因此有創建鏈接和關閉鏈接 的過程。

創建鏈接過程須要三步握手,以下:

  1. A向B發送syn信令
  2. B向A回覆ack,以及發送sync信令
  3. A向B回覆ack

其實網絡上發送數據都有可能丟的,因此每一個發送給對端的數據,要收到答覆才能確認 對方收到了。 好比上面第二步A收到了B返回的ack才能確認鏈接已經創建成功,本身給B發送數據,B 能夠收到,一樣第三步B收到A的ack才能確認鏈接創建成功,本身發給A的數據,A能收到。 因此TCP鏈接創建不是兩步握手,不是四步握手,而是三步握手。

鏈接創建成功後雙方就能夠互發psh信令來傳輸數據了,一樣發出去的psh數據,也須要 收到ack才能確認對方收到,不然就得等待超時後重發。

拆除鏈接須要四步握手, 由於TCP是雙工的,因此本身這邊關閉鏈接,有可能對方還會 給本身發數據,還得等對方說本身不會給本身發送數據了。

  1. A向B發送fin, 表示本身沒有數據向B發送了。
  2. B向A回覆ack
  3. B向A發送過fin, 表示本身沒有數據向A發送了。
  4. A向B回覆ack

另外就是在任什麼時候候均可能收到對方發來的rst信令,表示直接復位該鏈接,也別發數據了 也別等着收數據了,趕忙把資源都回收了吧。

TCP還有滑動窗口的流量控制機制,以及各類超時處理邏輯,有興趣的話具體細節看 《TCP/IP協議詳解》了。

linux下用tcpdump能夠抓包學習TCP協議,好比在執行curl -I www.baidu.com時用 tcpdump抓包以下。

# tcpdump -nn -t host www.baidu.com
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [S], seq 1772495094, win 14600, options [mss 1460,sackOK,TS val 214360452 ecr 0,nop,wscale 5], length 0
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [S.], seq 946873815, ack 1772495095, win 14600, options [mss 1440,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 7], length 0
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 1, win 457, length 0
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [P.], seq 1:168, ack 1, win 457, length 167
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 168, win 202, length 0
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [P.], seq 1:705, ack 168, win 202, length 704
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 705, win 501, length 0
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [F.], seq 168, ack 705, win 501, length 0
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [.], ack 169, win 202, length 0
IP 180.97.33.71.80 > 10.190.176.177.34840: Flags [F.], seq 705, ack 169, win 202, length 0
IP 10.190.176.177.34840 > 180.97.33.71.80: Flags [.], ack 706, win 501, length 0

 

能夠看到本機的ip是10.190.176.177,baidu解析出來的ip是180.97.33.71,而後前三個 包就是創建鏈接的三步握手,最後三個包是關閉鏈接的四步握手。中括號裏的S表示sync, p表示psh,F表示fin,.好像表示ack。

Linux基礎

其實Linux下,C的庫函數,以及linux API都在libc.so裏面,沒有分開 的。玩C語言開發,確定要對C庫函數和經常使用的linux API有所熟悉的,能夠先看 以下兩個連接快速瞭解一下,知道系統有哪些能力和輪子。

Standard C 語言標準函數庫速查 http://ganquan.info/standard-c/ Linux系統調用列表http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html

再就是系統調用,Linux API, 系統命令,和內核函數不是一回事,雖然他們有關聯。 系統調用是經過軟中斷向內核提交請求,獲取內核服務的接口,Linux Api則定義了一組 函數如read,malloc等,封裝了系統調用, 好比malloc函數會調用brk系統調用。 而後有系統命令則更高一級,如ls,hostname,則直接提供了一個可執行程序, 關於他們 的關係能夠閱讀下面這篇文章:

http://wenku.baidu.com/view/9e33f3e94afe04a1b071de81.html

C語言要想使用別人的東西,首先要包含別人提供的頭文件,使用linux api和c庫函數 也同樣,默認的這些頭文件都在/usr/include裏,本身安裝的一些則通常約定放在 /usr/local/include裏。寫代碼的過程當中若是遇到一些類型或函數不知道怎麼使用,直接 能夠在這裏面找到頭文件看源碼。

Linux下還有好多數據類型是在學普通C語言是沒見到過的,好比size_t,ssize_t,unit32_t 啥的, 這些其實都在普通數據類型的別名,通常在/usr/include/asm/types.h裏能夠看到 他們是怎麼被typedef的,使用這些類型主要是爲了提升可移植性,同時語義更加明確, 好比size_t在32位機器上定義爲uint,64位機器上定義爲ulong,使用size_t編寫的代碼 就能夠在32位機器和64位機器上良好運行。 還有size_t的意義更明確,它不是用來表示 普通的無符號數字概念的,而是表示sizeof返回的結果或者說是能訪問的體系內存的長度。

而後像uint32_t這種類型是爲了編寫出更明確的代碼,像C語言的類型,int, long等在 不一樣的機器上都有不一樣的長度,但uint32_t在啥機器上都是32位長的,有時候需求就是 這樣,就須要用這種數據類型了。

還有就是Linux系統函數調用失敗,大多數時候都會erron賦一個整數值,這個整數值能夠 表示不一樣的錯誤緣由,能夠在終端下運行man errno來查看詳細,另外好多系統函數均可以 用man來查看幫助的,有的裏面還有使用示例的,是學習linux編程的很好的工具。

還有一些系統函數設計的挺好,我總結了一些慣用法吧算是,本身設計函數也能夠學習

第一個是經過指針參數來獲取數據,由於好多函數的返回值是int類型,表示函數調用 是否成功,或錯誤碼,而這個函數自己的任務還要返回一些實質的信息,這時候就能夠 經過參數來填充數據,讓調用者拿到,好比accept函數的使用 (簡化後的僞代碼,不能執行):

struct sockaddr_in client;
if (accept(listenfd, &client) >= 0) {
    printf("%s\n", client);
}

 

這樣咱們調用一次函數,既能知道有沒有調用成功,成功的話又能拿到客戶端的描述符, 以及對端的網絡地址。

第二個是C沒有類和對象的概念,但也能夠模擬出來相似的概念,好比網絡編程,經過 socket函數建立一個描述符,好比說是fd,其實這就至關於一個類的實例,一個對象了, 而後調用read(fd),send(fd),close(fd)等函數來操做它,和麪向對象裏用fd.read(), fd.send(),fd.close()只是用法不一樣而已,因此寫C是能用獲得一些面向對象的思想的。

第三個是在Linux裏好多東西能夠用描述符來表示,好比文件,硬件端口,網絡鏈接等, 而後能夠針對描述符調用read,write等操做,這個是個很好的抽象,可使用很簡單的 幾個接口來實現很強大的功能,在寫本身的C軟件時也能夠借鑑這個思路。就是先創建一個 概念,而後寫不少的函數來操做這個概念,而不是創建不少的概念,你們記不住的。

第四個是,C其實沒有太多的類型檢查功能,表示複雜的數據都用struct表示,而不一樣的 struct是能夠強轉的,因此能夠用帶標誌的struct來表達相似面向對象多態的概念,如 bind函數須要一個struct sockaddr的參數,但ipv4和ipv6的地址分別用 struct sockaddr_in和struct sockaddr_in6表示,感受就至關於struct sockaddr的兩個 子結構,這樣bind函數就使用父結構struct sockaddr來同時支持ipv4和ipv6了。 須要注意子結構和父結構的標誌成員要放在最前面,這樣子結構轉成父結構時,父結構 才能正確的讀出標誌,從而在具體使用時強轉爲合適的子結構。

就這樣了,Linux編程入門我知道的就這些,更多可看《Unix環境高級編程》

socket基礎

先學一些socket客戶端編程來熟悉socket編程吧, 要鏈接到遠程主機,首要要 有個遠程主機的地址,一個遠程主機的地址包含對方的IP和端口,有時候咱們 只知道對方的域名,因此首先要解析出IP來,好多書上都是用gethostbyname來解析域名 的,但它過期了,不支持ipv6,並且參數不支持ip格式的字符串,返回的地址必須拷貝 後才能使用,不然同線程再調用一次該函數那地址就變了,總之是一個過期的函數了。

如今比較國際範的函數是getaddrinfo,能夠經過man查它的用法,

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

 

該函數同時支持 ipv4和v6,而後host支持域名也支持ip格式的字符串,hints用來設置 查詢的一些條件,result用來獲取查詢到的結果,他是一個指向指針的指針類型。

這至關也是一個慣用法了,一個參數用來講明調用需求,一個指針參數來獲取返回數據。 像select就是調用需求和返回數據都是一個參數來表示,但像pool就是調用需求和返回 用兩個參數了,更明確,前一個是const,後一個是指針。具體使用示例以下:

struct addrinfo* get_addr(const char *host, const char *port){
    struct addrinfo hints;     // 填充getaddrinfo參數
    struct addrinfo *result;   // 存放getaddrinfo返回數據

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = 0;
    hints.ai_protocol = 0;

    if(getaddrinfo(host, port, &hints, &result) != 0) {
        printf("getaddrinfo error");
        exit(1);
    }
    return result;
}

 

對了,getaddrinfo返回的result指向的內存是系統分配的,用完了要調用 freeaddrinfo去釋放內存的。其實getaddrinfo的內部實現挺複雜的,調用了一堆ga開頭 的函數,並且struct addrinfo其實也蠻複雜的,裏面有好多信息,但用好它是寫出 同時支持ipv4,ipv6網絡程序的關鍵。

建立socket, 要熟悉下family,socktype,protocol等概念和取值,查man吧

int create_socket(const struct addrinfo * result) {
    int fd;

    if ((fd = socket(result->ai_family, result->ai_socktype, result->ai_protocol)) == -1) {
        printf("create socket error:%d\n", fd);
        exit(-1);
    }
    printf("cerate socket ok: %d\n", fd);
    return fd;
}

 

鏈接目標主機, 這裏其實就是要三步握手了,有幾個常見的錯誤,能夠經過檢測errno來 讀取,如ETIMEDOUT表示創建鏈接超時,就是發出去sync沒人搭理,或ECONNREFUSED表示 對方端口沒開,發過去的sync直接被對方發了個rst回來,或EHOSTUNREACH表示對方機器 沒開或宕機了,由於ICMP包返回錯誤了。

int connect_host(int fd, const struct addrinfo* addr) {
    if (connect(fd , addr->ai_addr, addr->ai_addrlen) == -1) {
        printf("connect error.\n");
        exit(-1);
    }
    printf("collect ok\n");
    return 0;
}

 

咱們要作一個HTTP客戶端,相似curl,要拼一個HTTP請求發送給遠程主機,拼包用 snprintf雖然弱了一點,但也是最容易理解的,先用着。要留意格式化後的字符串大小 別超過緩衝區大小,固然了指定了長度不會溢出,但超事後會截斷,若是HTTP請求丟失 了最後的兩對\r\n,服務端就不知道客戶端發送完數據了, 因此這裏邊界處理要十分 當心,可能我這裏寫的還有BUG。

還有就是數據大的話send一次可能發送不完,這裏先簡單粗暴處理了一下,真實程序的 話要把剩下的半拉從新拷貝個buf發出去的。

int get_send_data(char * buf, size_t buf_size, const char* host) {
    const char *send_tpl;                        // 數據模板,%s是host佔位符 
    size_t to_send_size;                         // 要發送到數據大小 

    send_tpl = "GET / HTTP/1.1\r\n"
               "Host: %s\r\n"
               "Accept: */*\r\n"
               "\r\n\r\n";

    // 格式化後的長度必須小於buf的大小,由於snprintf會在最後填個'\0'
    if (strlen(host) + strlen(send_tpl) - 2 >= buf_size) { // 2 = strlen("%s")
        printf("host too long.\n");
        exit(-1);
    }

    to_send_size = snprintf(buf, buf_size, send_tpl, host);
    if (to_send_size < 0) {
        printf("snprintf error:%s.\n", to_send_size);
        exit(-2);
    }

    return to_send_size;
}

int send_data(int fd, const char *data, size_t size) {
    size_t sent_size;
    printf("will send:\n%s", data);
    sent_size = write(fd, data, size);
    if (sent_size < 0) {
        printf("send data error.\n");
        exit(-1);
    }else if(sent_size != size){
         printf("not all send.\n");
         exit(-2);
    }
    printf("send data ok.\n");
    return sent_size;
}

 

完了收數據,咱們只取HTTP應答第一行就行了,而後關閉鏈接。協議解析也簡單粗暴 找到\r\n就中止,真實程序可能要寫個狀態機來解析了。

int recv_data(int fd, char* buf, int size) {
    int i;
    int recv_size = read(fd, buf, size);
    if (recv_size < 0) {
        printf("recv data error:%d\n", (int)recv_size);
        exit(-1);
    }
    if (recv_size == 0) {
        printf("recv 0 size data.\n");
        exit(-2);
    }
    // 只取HTTP first line
    for (i = 0; i < size - 1; i++) {
        if (buf[i] == '\r' && buf[i+1] == '\n') {
            buf[i] = '\0';
        }
    }
    printf("recv data:%s\n", buf);
}

int close_socket(int fd) {
    if(close(fd) < 0){
         printf("close socket errors\n");
         exit(-1);
    }
    printf("close socket ok\n");
}

 

最後用main函數把他們串起來

int main(int argc, const char *argv[])
{
    const char* host = argv[1];                  // 目標主機
    char send_buff[SEND_BUF_SIZE];               // 發送緩衝區
    char recv_buf[RECV_BUFF_SIZE];               // 接收緩衝區
    size_t to_send_size = 0;                     // 要發送數據大小 
    int client_fd;                               // 客戶端socket
    struct addrinfo *addr;                       // 存放getaddrinfo返回數據

    if (argc != 2) {
        printf("Usage:%s [host]\n", argv[0]);
        return 1;
    }


    addr = get_addr(host, "80");
    client_fd = create_socket(addr);
    connect_host(client_fd, addr);
    freeaddrinfo(addr);

    to_send_size = get_send_data(send_buff, SEND_BUF_SIZE, host);
    send_data(client_fd, send_buff, to_send_size);

    recv_data(client_fd, recv_buf, RECV_BUFF_SIZE);

    close(client_fd);
    return 0;
}

 

小結

多看,多寫,多練,確定能熟悉C語言的,我如今看好多C的書都能看懂了。

相關文章
相關標籤/搜索