鍍金池/ 教程/ C/ 0x11-套接字編程-1
0x0E-單線程備份(下)
0x11-套接字編程-1
0x05-C語(yǔ)言指針:(Volume-1)
0x13-套接字編程-HTTP服務(wù)器(1)
0x0C-開始行動(dòng)
C 語(yǔ)言進(jìn)階
第一部分
0x05-C語(yǔ)言指針(Volume-2)
0x08-C語(yǔ)言效率(下)
0x07-C語(yǔ)言效率(上)
0x04 C代碼規(guī)范
0x0F-多線程備份
0x05-C語(yǔ)言變量
第四部分
0x16-套接字編程-HTTP服務(wù)器(4)
0x0D-單線程備份(上)
總結(jié)
0x01-C語(yǔ)言序言
0x15-套接字編程-HTTP服務(wù)器(3)
0x14-套接字編程-HTTP服務(wù)器(2)
0x17-套接字編程-HTTP服務(wù)器(5)
第三部分
我的C語(yǔ)言
0x06-C語(yǔ)言預(yù)處理器
0x09-未曾領(lǐng)略的新風(fēng)景
0x0A-C線程和Glib的視角
第二部分
0x10-網(wǎng)絡(luò)的世界
0x12-套接字編程-2
0x03-C代碼
0x0B-C語(yǔ)言錯(cuò)誤處理

0x11-套接字編程-1

0x11-套接字編程-1

套接字編程

  • 兩種協(xié)議 TCPUDP
    • 前者可以理解為有保證的連接,后者是追求快速的連接
    • 當(dāng)然最后一點(diǎn)有些 太過絕對(duì) ,但是現(xiàn)在不需熬考慮太多,因?yàn)槌跞胩捉幼志幊蹋磺袕暮?jiǎn)
    • 稍微試想便能夠大致理解, TCP 追求的是可靠的傳輸數(shù)據(jù), UDP 追求的則是快速的傳輸數(shù)據(jù)
    • 前者有繁瑣的連接過程,后者則是根本不建立可靠連接(不是絕對(duì)),只是將數(shù)據(jù)發(fā)送而不考慮是否到達(dá)。

以下例子以 *nix 平臺(tái)的便準(zhǔn)為例,因?yàn)?Windows平臺(tái)需要考慮額外的加載問題,稍作添加就能在 Windows 平臺(tái)上運(yùn)行

UDP

  • UDP

    • 這是一個(gè)十分簡(jiǎn)潔的連接方式,假設(shè)有兩臺(tái)主機(jī)進(jìn)行通信,一臺(tái)只發(fā)送,一臺(tái)只接收。
    • 接收端:

          int sock; /* 套接字 */
          socklen_t addr_len; /* 發(fā)送端的地址長(zhǎng)度,用于 recvfrom */
          char mess[15];
          char get_mess[GET_MAX]; /* 后續(xù)版本使用 */
          struct sockaddr_in recv_host, send_host;
      
          /* 創(chuàng)建套接字 */
          sock = socket(PF_INET, SOCK_DGRAM, 0);
      
          /* 把IP 和 端口號(hào)信息綁定在套接字上 */
          memset(&recv_host, 0, sizeof(recv_host));
          recv_host.sin_family = AF_INET;
          recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
          recv_host.sin_port = htons(6000); /* 使用6000 端口號(hào) */
          bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
      
          /* 進(jìn)入接收信息的狀態(tài) */
          recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
      
          /* 接收完成,關(guān)閉套接字 */
          close(sock);

      上述代碼省略了許多必要的 錯(cuò)誤檢查 ,在實(shí)際編寫時(shí)要添加

    • 代碼解釋:
      1. PF_INET 代表協(xié)議的類型,此處代表 IPv4 網(wǎng)絡(luò)協(xié)議族, 同樣 PF_INET6 代表 IPv6 網(wǎng)絡(luò)協(xié)議族,這個(gè)范圍在后方單獨(dú)記錄,不與IPv4混在一起(并不意味著更復(fù)雜,實(shí)際上更簡(jiǎn)便)。
      2. AF_INET 代表地址的類型,此處代表 IPv4 網(wǎng)絡(luò)協(xié)議使用的地址族, 同樣有 AF_INET6 (在操作系統(tǒng)實(shí)現(xiàn)中 PF_INET 和 AF_INET 的值一樣,但是還是要寫宏更好,而不應(yīng)該直接用數(shù)字或者,混淆使用)
      3. htonlhtons 兩個(gè)函數(shù)的使用涉及到 大端小端問題, 這里不敘述,需要記住的是在網(wǎng)絡(luò)編程時(shí)一定要使用這種函數(shù)將必要信息轉(zhuǎn)為 大端表示法
      4. (struct sockaddr *) 這個(gè)強(qiáng)制轉(zhuǎn)換是為了參數(shù)的必須,但不會(huì)出錯(cuò),因?yàn)?sizeof(struct sockaddr_in) == sizeof(struct sockaddr) 具體可以查詢相關(guān)信息,之所以這么做是為了方便編寫套接字程序的程序員。
    • 發(fā)送端:

          int sock;
          const char* mess = "Hello Server!";
          char get_mess[GET_MAX]; /* 后續(xù)版本使用 */
          struct sockaddr_in recv_host;
          socklen_t addr_len;
          /* 創(chuàng)建套接字 */
          sock = socket(PF_INET, SOCK_DGRAM, 0);
          /* 綁定 */
          memset(&recv_host, 0, sizeof(recv_host));
          recv_host.sin_family = AF_INET;
          recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
          recv_host.sin_port = htons(6000);
          /* 發(fā)送信息 */
          /* 在此處,發(fā)送端的IP地址和端口號(hào)等各類信息,隨著這個(gè)函數(shù)的調(diào)用,自動(dòng)綁定在了套接字上 */
          sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
          /* 完成,關(guān)閉 */
          close(sock);

      上述代碼是發(fā)送端。

    • 代碼解釋:
      1. inet_addr 函數(shù)是用于將字符串格式的 IP地址 轉(zhuǎn)換為 大端表示法的 地址類型,即 s_addr 的類型 in_addr_t
      2. 與之相反,同樣也有功能相反的函數(shù) inet_ntoa 用于將 in_addr_t 類型轉(zhuǎn)為字符串,但是使用時(shí)一定要記住及時(shí)拷貝返回值 char addr[16]; recv_host.sin_addr.s_addr = inet_addr("127.0.0.1"); strcpy(addr, inet_ntoa(recv_host.sin_addr.s_addr));
    • 從上述代碼看出, UDP 協(xié)議的使用十分簡(jiǎn)潔,幾乎就是 創(chuàng)建套接字->準(zhǔn)備數(shù)據(jù)->裝備套接字->發(fā)送/接收->結(jié)束
    • 其中,都沒有連接的操作,但是實(shí)際上這是為了方便 UDP 隨時(shí)和 不同的主機(jī) 進(jìn)行通信所默認(rèn)的設(shè)置,如果需要和相同主機(jī)一直通信呢?
    • 此中的原由暫時(shí)不需要知道,記錄方法,即長(zhǎng)時(shí)間使用 UDP 和同一主機(jī)通信時(shí),可以使用 connect 函數(shù)來進(jìn)行優(yōu)化自身。此時(shí) 假設(shè)兩臺(tái)主機(jī)的實(shí)際功能一致,既接收也發(fā)送
    • 發(fā)送端:

          /* 前方高度一致,將 bind函數(shù)替換為 */
          connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 將對(duì)方的 IP地址和 端口號(hào)信息 注冊(cè)進(jìn)UDP的套接字中)
          while(1) /* 循環(huán)的發(fā)送和接收信息 */
          {
            size_t read_len = 0;
            /* 原先使用的 sendto 函數(shù),先擇改為使用 write 函數(shù), Windows平臺(tái)為 send 函數(shù) */
            write(sock, mess, strlen(mess));            /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
            read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
            get_mess[read_len-1] = '\0';
            printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
          }
          /* 后方高度一致 */
    • 接收端:

          /* 前方一致, 添加額外的 struct sockaddr_in send_host; 并添加循環(huán),構(gòu)造收發(fā)的現(xiàn)象*/
              while(1)
          {
            size_t read_len = 0;
            char sent_mess[15] = "Hello Sender!"; /* 用于發(fā)送的信息  */
            sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
            read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
            mess[read_len-1] = '\0';
            printf("In Sever like Host Recvive From other Host : %s\n", mess);
          }
          /* 后方高度一致 */
          /*
          * 之所以只在接收端使用 connect 的原因,便在于我們模擬的是 客戶端-服務(wù)器 的模型,而服務(wù)器的各項(xiàng)信息是不會(huì)隨意變更的
          * 但是 客戶端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能總是固定的,所以只能
          * 保證 在客戶端 部分注冊(cè)了 服務(wù)器 的各類信息,而不能在 服務(wù)器端 注冊(cè) 客戶端 的信息。
          * 當(dāng)然也有例外,例如你就想這個(gè)軟件作為私密軟件,僅供兩個(gè)人使用, 且你有固定的 IP地址,那么你可以兩邊都connect,但是
          * 一定要注意,只要有一點(diǎn)信息變動(dòng),這個(gè)軟件就可能無法正常的收發(fā)信息了。
          */
  • 代碼解釋
    • 故而實(shí)際上,雖然前方的表格顯示,UDP 似乎并沒有 connect 的使用必要,但是實(shí)際上還是有用到的地方。
    • *nixAPI 來說,sendtowrite 的區(qū)別十分明顯,便是一個(gè)需要在參數(shù)中提供目標(biāo)主機(jī)的各類信息,而后者則不需要提供。同樣的道理recvfromread也是如此。
    • 這個(gè)代碼只是做演示而已,所以將代碼置于無限循環(huán)當(dāng)中,現(xiàn)實(shí)中可以自行定義出口條件。

以上是 UDP 的一些簡(jiǎn)單說明,入門足矣,并未詳細(xì)敘述某些 函數(shù) 的具體用法,而是用實(shí)際例子來體現(xiàn)。 在 記錄 TCP 之前,還是需要講一個(gè)函數(shù) shutdown

  • shutdownclose(closesocket)

    • 首先要知道,網(wǎng)絡(luò)通信一般而言是雙方的共同進(jìn)行的,換而言之就是雙向的,一個(gè)方向只用來發(fā)送消息,一個(gè)方向只用來讀取消息。
    • 這就導(dǎo)致了,在結(jié)束套接字通信的時(shí)候,需要關(guān)閉兩個(gè)方向的通道(暫時(shí)叫它們通道),那同時(shí)關(guān)閉不行嗎?可以啊
      • close(sock); // closesocket(sock); FOR Windows PlatForm 就是這么干的,同時(shí)斷開兩個(gè)方向的連接。
      • 簡(jiǎn)單的通信程序或者單向通信程序這么做的確無甚大礙,但是萬(wàn)一在結(jié)束通信的時(shí)候需要接收最后一個(gè)信息那該怎么辦?
        • 假設(shè)通信結(jié)束,客戶端向服務(wù)器發(fā)送 "Thank you"
        • 服務(wù)器需要接收這個(gè)信息,之后才能關(guān)閉通信
        • 問題就在這之間,服務(wù)器并不知道客戶端會(huì)在通信結(jié)束后的什么時(shí)刻傳來信息
        • 所以我們選擇在通信完成后先關(guān)閉 服務(wù)器的 發(fā)送通道(寫流),等待客戶端發(fā)來消息后,關(guān)閉剩下的 接收通道(讀流)
    • 發(fā)送端:

          /* 假設(shè)有一個(gè) TCP 的連接,此為客戶端 */
          write(sock, "Thank you", 10);
          close(sock); // 寫完直接關(guān)閉通信
    • 接收端:

          /* 此為服務(wù)器 */
          /* 首先關(guān)閉寫流 */
          shutdown(sock_c, SHUT_WR);
          read(sock_c, get_mess, GET_MAX);
          printf("Message : %s\n", get_mess);
          close(sock_c);
          close(sock_s); // 關(guān)閉兩個(gè)套接字是因?yàn)?TCP 服務(wù)器端的需要,后續(xù)會(huì)記錄
    • 代碼解釋
      • shutdown 函數(shù)的作用就是 可選擇的關(guān)閉那個(gè)方向的輸出
        • int shutdown(int sock, int howto);
        • sock 代表要操作的套接字
        • howto有幾個(gè)選擇
          • * nix ** : SHUT_RD SHUT_WR SHUT_RDWR
          • Windows : SD_RECEIVE SD_SEND SD_BOTH

停下來

  1. 程序員應(yīng)該越來越來,做的事情應(yīng)該越來越少,但是能達(dá)到的成就應(yīng)該越來越多
  2. 在 IPv6 出現(xiàn)的今天,網(wǎng)絡(luò)編程已經(jīng)開始向簡(jiǎn)潔和強(qiáng)大靠近,即便是身為底層語(yǔ)言的 C語(yǔ)言
  3. 實(shí)際上由于 C語(yǔ)言 并沒有自己的網(wǎng)絡(luò)庫(kù), 故為了能進(jìn)行網(wǎng)絡(luò)編程,不得不依賴于系統(tǒng)函數(shù),這就是所謂的系統(tǒng)編程, 你已經(jīng)是一個(gè)系統(tǒng)程序員了。
  4. 而 系統(tǒng)函數(shù) 隨著時(shí)代的變化,正在不斷完善,增加(幾乎沒有廢除的先例,所以不用擔(dān)心之前的程序無法運(yùn)行)。
  5. 相應(yīng)的,由于以前的網(wǎng)絡(luò)編程只適合于 IPv4 的地址,自從出現(xiàn)了 IPv6, 我們需要一套全新的方式,正好他來了。