电竞比分网-中国电竞赛事及体育赛事平台

分享

快速學(xué)習(xí)C語言三: 開發(fā)環(huán)境, VIM配置, TCP基礎(chǔ),Linux開發(fā)基礎(chǔ),Socket開發(fā)基礎(chǔ)

 書*金 2015-01-30

上次學(xué)了一些C開發(fā)相關(guān)的工具,這次再配置一下VIM,讓開發(fā)過程更爽一些。 另外再學(xué)一些linux下網(wǎng)絡(luò)開發(fā)的基礎(chǔ),好多人學(xué)C也是為了做網(wǎng)絡(luò)開發(fā)。

開發(fā)環(huán)境

首先得有個Linux環(huán)境,有時候家里機器是Windows,裝虛擬機也麻煩,所以還不如30塊錢 買個騰訊云,用putty遠程練上去寫代碼呢。

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

買好騰訊云后,裝個Centos,會分配個外網(wǎng)IP,然后買個域名,在DNSPod解析過去,就 可以用putty遠程登錄了,putty一般做如下設(shè)置。

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

設(shè)置完成后把這個會話起個名字,比如叫qcloud,下次用的時候先加載,然后open 就可以了, 所有設(shè)置會保存起來。這樣配置后putty已經(jīng)很好用了,但我們還可以搞成 自動登錄,不需要每次都輸入密碼。

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

下次雙擊快捷方式就登錄上去了,而且上面的設(shè)置都會生效。對了,putty和puttygen 要在官方下載哦。

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指定成剛安裝的版本

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

 

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

復(fù)制代碼
" 基本設(shè)置
set nocp
set ts=4
set sw=4
set smarttab
set et
set ambiwidth=double
set nu

" 編碼設(shè)置 
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
復(fù)制代碼

 

基本每一個VIM最少要配置成這樣,包括生產(chǎn)環(huán)境,前半拉主要是設(shè)置縮進成4個空格, 后半拉是設(shè)置編碼,以便打開文件時不會亂碼。

如果想開發(fā)時更爽一些,就得裝插件了,現(xiàn)在裝插件也很簡單,先裝插件管理工具 pathogen.vim, 如下

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

 

然后安裝一個文件模版插件,一個代碼片段插件,一個智能體似乎插件就可以了,傻瓜式 的,如下

復(fù)制代碼
# 安裝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
復(fù)制代碼

 

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

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

 

別的插件能不裝就不裝了吧,用的時候再說,現(xiàn)在你打開一個新的.c文件,會自動從模版 里加載一個代碼框架進來,然后輸入main,for,pr等按tab鍵就會自動生成代碼片段, 然后include頭文件后,里面的函數(shù),類型等在輸入時按ctrl+n就會自動提示,結(jié)構(gòu)的 成員也可以,已經(jīng)很爽了。

TCP基礎(chǔ)

TCP使用很廣泛,先了解一下概念,TCP是面向連接的協(xié)議,所以有建立連接和關(guān)閉連接 的過程。

建立連接過程需要三步握手,如下:

  1. A向B發(fā)送syn信令
  2. B向A回復(fù)ack,以及發(fā)送sync信令
  3. A向B回復(fù)ack

其實網(wǎng)絡(luò)上發(fā)送數(shù)據(jù)都有可能丟的,所以每個發(fā)送給對端的數(shù)據(jù),要收到答復(fù)才能確認(rèn) 對方收到了。 比如上面第二步A收到了B返回的ack才能確認(rèn)連接已經(jīng)建立成功,自己給B發(fā)送數(shù)據(jù),B 可以收到,同樣第三步B收到A的ack才能確認(rèn)連接建立成功,自己發(fā)給A的數(shù)據(jù),A能收到。 所以TCP連接建立不是兩步握手,不是四步握手,而是三步握手。

連接建立成功后雙方就可以互發(fā)psh信令來傳輸數(shù)據(jù)了,同樣發(fā)出去的psh數(shù)據(jù),也需要 收到ack才能確認(rèn)對方收到,否則就得等待超時后重發(fā)。

拆除連接需要四步握手, 因為TCP是雙工的,所以自己這邊關(guān)閉連接,有可能對方還會 給自己發(fā)數(shù)據(jù),還得等對方說自己不會給自己發(fā)送數(shù)據(jù)了。

  1. A向B發(fā)送fin, 表示自己沒有數(shù)據(jù)向B發(fā)送了。
  2. B向A回復(fù)ack
  3. B向A發(fā)送過fin, 表示自己沒有數(shù)據(jù)向A發(fā)送了。
  4. A向B回復(fù)ack

另外就是在任何時候都可能收到對方發(fā)來的rst信令,表示直接復(fù)位該連接,也別發(fā)數(shù)據(jù)了 也別等著收數(shù)據(jù)了,趕緊把資源都回收了吧。

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

linux下用tcpdump可以抓包學(xué)習(xí)TCP協(xié)議,比如在執(zhí)行curl -I www.baidu.com時用 tcpdump抓包如下。

復(fù)制代碼
# 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
復(fù)制代碼

 

可以看到本機的ip是10.190.176.177,baidu解析出來的ip是180.97.33.71,然后前三個 包就是建立連接的三步握手,最后三個包是關(guān)閉連接的四步握手。中括號里的S表示sync, p表示psh,F(xiàn)表示fin,.好像表示ack。

Linux基礎(chǔ)

其實Linux下,C的庫函數(shù),以及l(fā)inux API都在libc.so里面,沒有分開 的。玩C語言開發(fā),肯定要對C庫函數(shù)和常用的linux API有所熟悉的,可以先看 如下兩個鏈接快速了解一下,知道系統(tǒng)有哪些能力和輪子。

Standard C 語言標(biāo)準(zhǔn)函數(shù)庫速查 http:///standard-c/ Linux系統(tǒng)調(diào)用列表http://www.ibm.com/developerworks/cn/linux/kernel/syscall/part1/appendix.html

再就是系統(tǒng)調(diào)用,Linux API, 系統(tǒng)命令,和內(nèi)核函數(shù)不是一回事,雖然他們有關(guān)聯(lián)。 系統(tǒng)調(diào)用是通過軟中斷向內(nèi)核提交請求,獲取內(nèi)核服務(wù)的接口,Linux Api則定義了一組 函數(shù)如read,malloc等,封裝了系統(tǒng)調(diào)用, 比如malloc函數(shù)會調(diào)用brk系統(tǒng)調(diào)用。 然后有系統(tǒng)命令則更高一級,如ls,hostname,則直接提供了一個可執(zhí)行程序, 關(guān)于他們 的關(guān)系可以閱讀下面這篇文章:

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

C語言要想使用別人的東西,首先要包含別人提供的頭文件,使用linux api和c庫函數(shù) 也一樣,默認(rèn)的這些頭文件都在/usr/include里,自己安裝的一些則一般約定放在 /usr/local/include里。寫代碼的過程中如果遇到一些類型或函數(shù)不知道怎么使用,直接 可以在這里面找到頭文件看源碼。

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

然后像uint32_t這種類型是為了編寫出更明確的代碼,像C語言的類型,int, long等在 不同的機器上都有不同的長度,但uint32_t在啥機器上都是32位長的,有時候需求就是 這樣,就需要用這種數(shù)據(jù)類型了。

還有就是Linux系統(tǒng)函數(shù)調(diào)用失敗,大多數(shù)時候都會erron賦一個整數(shù)值,這個整數(shù)值可以 表示不同的錯誤原因,可以在終端下運行man errno來查看詳細,另外好多系統(tǒng)函數(shù)都可以 用man來查看幫助的,有的里面還有使用示例的,是學(xué)習(xí)linux編程的很好的工具。

還有一些系統(tǒng)函數(shù)設(shè)計的挺好,我總結(jié)了一些慣用法吧算是,自己設(shè)計函數(shù)也可以學(xué)習(xí)

第一個是通過指針參數(shù)來獲取數(shù)據(jù),因為好多函數(shù)的返回值是int類型,表示函數(shù)調(diào)用 是否成功,或錯誤碼,而這個函數(shù)本身的任務(wù)還要返回一些實質(zhì)的信息,這時候就可以 通過參數(shù)來填充數(shù)據(jù),讓調(diào)用者拿到,比如accept函數(shù)的使用 (簡化后的偽代碼,不能執(zhí)行):

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

 

這樣我們調(diào)用一次函數(shù),既能知道有沒有調(diào)用成功,成功的話又能拿到客戶端的描述符, 以及對端的網(wǎng)絡(luò)地址。

第二個是C沒有類和對象的概念,但也可以模擬出來類似的概念,比如網(wǎng)絡(luò)編程,通過 socket函數(shù)創(chuàng)建一個描述符,比如說是fd,其實這就相當(dāng)于一個類的實例,一個對象了, 然后調(diào)用read(fd),send(fd),close(fd)等函數(shù)來操作它,和面向?qū)ο罄镉胒d.read(), fd.send(),fd.close()只是用法不同而已,所以寫C是能用得到一些面向?qū)ο蟮乃枷氲摹?/p>

第三個是在Linux里好多東西可以用描述符來表示,比如文件,硬件端口,網(wǎng)絡(luò)連接等, 然后可以針對描述符調(diào)用read,write等操作,這個是個很好的抽象,可以使用很簡單的 幾個接口來實現(xiàn)很強大的功能,在寫自己的C軟件時也可以借鑒這個思路。就是先建立一個 概念,然后寫很多的函數(shù)來操作這個概念,而不是建立很多的概念,大家記不住的。

第四個是,C其實沒有太多的類型檢查功能,表示復(fù)雜的數(shù)據(jù)都用struct表示,而不同的 struct是可以強轉(zhuǎn)的,所以可以用帶標(biāo)志的struct來表達類似面向?qū)ο蠖鄳B(tài)的概念,如 bind函數(shù)需要一個struct sockaddr的參數(shù),但ipv4和ipv6的地址分別用 struct sockaddr_in和struct sockaddr_in6表示,感覺就相當(dāng)于struct sockaddr的兩個 子結(jié)構(gòu),這樣bind函數(shù)就使用父結(jié)構(gòu)struct sockaddr來同時支持ipv4和ipv6了。 需要注意子結(jié)構(gòu)和父結(jié)構(gòu)的標(biāo)志成員要放在最前面,這樣子結(jié)構(gòu)轉(zhuǎn)成父結(jié)構(gòu)時,父結(jié)構(gòu) 才能正確的讀出標(biāo)志,從而在具體使用時強轉(zhuǎn)為合適的子結(jié)構(gòu)。

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

socket基礎(chǔ)

先學(xué)一些socket客戶端編程來熟悉socket編程吧, 要連接到遠程主機,首要要 有個遠程主機的地址,一個遠程主機的地址包含對方的IP和端口,有時候我們 只知道對方的域名,所以首先要解析出IP來,好多書上都是用gethostbyname來解析域名 的,但它過時了,不支持ipv6,而且參數(shù)不支持ip格式的字符串,返回的地址必須拷貝 后才能使用,否則同線程再調(diào)用一次該函數(shù)那地址就變了,總之是一個過時的函數(shù)了。

現(xiàn)在比較國際范的函數(shù)是getaddrinfo,可以通過man查它的用法,

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

 

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

這相當(dāng)也是一個慣用法了,一個參數(shù)用來說明調(diào)用需求,一個指針參數(shù)來獲取返回數(shù)據(jù)。 像select就是調(diào)用需求和返回數(shù)據(jù)都是一個參數(shù)來表示,但像pool就是調(diào)用需求和返回 用兩個參數(shù)了,更明確,前一個是const,后一個是指針。具體使用示例如下:

復(fù)制代碼
struct addrinfo* get_addr(const char *host, const char *port){
    struct addrinfo hints;     // 填充getaddrinfo參數(shù)
    struct addrinfo *result;   // 存放getaddrinfo返回數(shù)據(jù)

    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;
}
復(fù)制代碼

 

對了,getaddrinfo返回的result指向的內(nèi)存是系統(tǒng)分配的,用完了要調(diào)用 freeaddrinfo去釋放內(nèi)存的。其實getaddrinfo的內(nèi)部實現(xiàn)挺復(fù)雜的,調(diào)用了一堆ga開頭 的函數(shù),而且struct addrinfo其實也蠻復(fù)雜的,里面有好多信息,但用好它是寫出 同時支持ipv4,ipv6網(wǎng)絡(luò)程序的關(guān)鍵。

創(chuàng)建socket, 要熟悉下family,socktype,protocol等概念和取值,查man吧

復(fù)制代碼
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;
}
復(fù)制代碼

 

連接目標(biāo)主機, 這里其實就是要三步握手了,有幾個常見的錯誤,可以通過檢測errno來 讀取,如ETIMEDOUT表示建立連接超時,就是發(fā)出去sync沒人搭理,或ECONNREFUSED表示 對方端口沒開,發(fā)過去的sync直接被對方發(fā)了個rst回來,或EHOSTUNREACH表示對方機器 沒開或宕機了,因為ICMP包返回錯誤了。

復(fù)制代碼
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;
}
復(fù)制代碼

 

我們要做一個HTTP客戶端,類似curl,要拼一個HTTP請求發(fā)送給遠程主機,拼包用 snprintf雖然弱了一點,但也是最容易理解的,先用著。要留意格式化后的字符串大小 別超過緩沖區(qū)大小,當(dāng)然了指定了長度不會溢出,但超過后會截斷,如果HTTP請求丟失 了最后的兩對\r\n,服務(wù)端就不知道客戶端發(fā)送完數(shù)據(jù)了, 所以這里邊界處理要十分 小心,可能我這里寫的還有BUG。

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

復(fù)制代碼
int get_send_data(char * buf, size_t buf_size, const char* host) {
    const char *send_tpl;                        // 數(shù)據(jù)模板,%s是host占位符 
    size_t to_send_size;                         // 要發(fā)送到數(shù)據(jù)大小 

    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;
}
復(fù)制代碼

 

完了收數(shù)據(jù),我們只取HTTP應(yīng)答第一行就好了,然后關(guān)閉連接。協(xié)議解析也簡單粗暴 找到\r\n就停止,真實程序可能要寫個狀態(tài)機來解析了。

復(fù)制代碼
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");
}
復(fù)制代碼

 

最后用main函數(shù)把他們串起來

復(fù)制代碼
int main(int argc, const char *argv[])
{
    const char* host = argv[1];                  // 目標(biāo)主機
    char send_buff[SEND_BUF_SIZE];               // 發(fā)送緩沖區(qū)
    char recv_buf[RECV_BUFF_SIZE];               // 接收緩沖區(qū)
    size_t to_send_size = 0;                     // 要發(fā)送數(shù)據(jù)大小 
    int client_fd;                               // 客戶端socket
    struct addrinfo *addr;                       // 存放getaddrinfo返回數(shù)據(jù)

    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;
}
復(fù)制代碼

 

小結(jié)

多看,多寫,多練,肯定能熟悉C語言的,我現(xiàn)在看好多C的書都能看懂了。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多