上次學了一些C開發相關的工具,此次再配置一下VIM,讓開發過程更爽一些。 另外再學一些linux下網絡開發的基礎,好多人學C也是爲了作網絡開發。html
首先得有個Linux環境,有時候家裏機器是Windows,裝虛擬機也麻煩,因此還不如30塊錢 買個騰訊雲,用putty遠程練上去寫代碼呢。node
我一直都是putty+VIM在Linux下開發代碼,好幾年了,只要把putty和VIM配置好,其實 開發效率挺高的。python
買好騰訊雲後,裝個Centos,會分配個外網IP,而後買個域名,在DNSPod解析過去,就 能夠用putty遠程登陸了,putty通常作以下設置。linux
設置完成後把這個會話起個名字,好比叫qcloud,下次用的時候先加載,而後open 就能夠了, 全部設置會保存起來。這樣配置後putty已經很好用了,但咱們還能夠搞成 自動登陸,不須要每次都輸入密碼。git
下次雙擊快捷方式就登陸上去了,並且上面的設置都會生效。對了,putty和puttygen 要在官方下載哦。github
首先安裝最新的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是面向鏈接的協議,因此有創建鏈接和關閉鏈接 的過程。
創建鏈接過程須要三步握手,以下:
其實網絡上發送數據都有可能丟的,因此每一個發送給對端的數據,要收到答覆才能確認 對方收到了。 好比上面第二步A收到了B返回的ack才能確認鏈接已經創建成功,本身給B發送數據,B 能夠收到,一樣第三步B收到A的ack才能確認鏈接創建成功,本身發給A的數據,A能收到。 因此TCP鏈接創建不是兩步握手,不是四步握手,而是三步握手。
鏈接創建成功後雙方就能夠互發psh信令來傳輸數據了,一樣發出去的psh數據,也須要 收到ack才能確認對方收到,不然就得等待超時後重發。
拆除鏈接須要四步握手, 由於TCP是雙工的,因此本身這邊關閉鏈接,有可能對方還會 給本身發數據,還得等對方說本身不會給本身發送數據了。
另外就是在任什麼時候候均可能收到對方發來的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下,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編程吧, 要鏈接到遠程主機,首要要 有個遠程主機的地址,一個遠程主機的地址包含對方的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的書都能看懂了。