天天看點

TCP/IP 網絡程式設計入門(一)

網絡程式設計中接受連接配接請求(伺服器端)的套接字建立過程:

  1. 調用

    socket

    函數建立套接字
  2. 調用

    bind

    函數配置設定

    IP

    位址和端口号
  3. 調用

    listen

    函數轉為可接收請求狀态
  4. 調用

    accept

    函數受理連接配接請求

基于 Linux 中的檔案操作

打開檔案

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char * path, int flag) //成功時傳回檔案描述符,失敗傳回 -1

~ path: 檔案名的字元串位址
~ flag: 檔案打開模式資訊
           

關閉檔案

#include <unistd.h>

int close(int fd); //成功傳回0, 失敗傳回 -1
           

将資料寫入檔案

#include <unistd.h>
ssize_t write(int fd, const void * buf, size_t nbytes);

~ fd: 要寫入的檔案描述符
~ buf: 儲存要傳輸資料的緩沖位址值
~ nbytes: 要傳輸資料的位元組數
           

讀取檔案中的資料

#include <unistd.h>
ssize_t read(int fd, void * buf, size_t nbytes); //成功時傳回接收的位元組數(遇到檔案結束符傳回0),失敗傳回 -1
           

套接字類型和協定設定

建立套接字

#include <sys/socket.h>
int socket(int domin, int type, int protocol); // 成功傳回檔案描述符,失敗傳回 -1

~ domin: 套接字使用的協定族資訊,PF_INET(IPv4)  PF_INET6(IPv6)
~ type: 資料傳輸類型, 面向連接配接(SOCK_STREAM)  面向消息(SOCK_DGRAM)
~ protocol: 計算機間通信中使用的協定資訊
           

位址族與資料序列

表示IPv4位址的結構體

struct sockaddr_in
{
    sa_family_t       sin_family; // 位址族
    uint16_t          sin_port;   // 16位端口号
    struct in_addr    sin_addr;   // 32位IP位址
    char              sin_zero[];// 不使用
}

struct in_addr
{
    In_addr_t         s_addr;     // 32 位IPv4 位址
}
           

sin_zero

隻為了是結構體

sockaddr_in

的大小和

sockaddr

一緻而插入的成員。

struct sockaddr
{
    sa_family_t     sin_family;
    char            sa_data[]; //位址資訊
}
           

網絡位元組序和位址轉換

  1. 小端法(

    Little-Endian

    )就是

    低位位元組(0x78)排放在記憶體的低位址端

    即該值的起始位址,高位位元組排放在記憶體的高位址端。
  2. 大端法(

    Big-Endian

    )就是高位位元組排放在記憶體的低位址端即該值的起始位址,低位位元組排放在記憶體的高位址端。
TCP/IP 網絡程式設計入門(一)

檢測計算機是大位元組序還是小位元組序:

#include <stdio.h>
#include <netinet/in.h>
int main()
{
int i_num = ;
printf("[0]:0x%x\n", *((char *)&i_num + ));
printf("[1]:0x%x\n", *((char *)&i_num + ));
printf("[2]:0x%x\n", *((char *)&i_num + ));
printf("[3]:0x%x\n", *((char *)&i_num + ));
i_num = htonl(i_num);
printf("[0]:0x%x\n", *((char *)&i_num + ));
printf("[1]:0x%x\n", *((char *)&i_num + ));
printf("[2]:0x%x\n", *((char *)&i_num + ));
printf("[3]:0x%x\n", *((char *)&i_num + ));

return ;
}
           

每種

CPU

的儲存方式都不相同,是以經過網絡轉換也必須保證資訊位元組序的正确性。

在網絡傳輸的時候統一為大端序

以下是位元組序轉換的函數:

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htosl(unsigned long);
unsigned long ntohl(unsigned long);

h 代表主機(host)位元組序;n 代表網絡位元組序;s 指 short(兩位元組,端口);l 指 long(四位元組,IP);

htons: 把 short 型資料從主機位元組序轉換為網絡位元組序
           

位址格式的轉換

sockaddr_in

中儲存位址資訊的成員為32位整數,是以需要将

IP

位址儲存為32位整數型資料。

#include <arpa/inet.h>

in_addr_t inet_addr(const char * string); //将點分十進制轉換成32位整數,失敗時傳回 INADDR_NONE
           
#include <arpa/inet.h>

int inet_aton(const char * string, struct in_addr * addr); //跟上面的函數作用相同,隻不過是填充到結構體中.成功傳回1 ,失敗傳回0
           
#include <arpa/inet.h>

char * inet_ntoa(struct in_addr adr); //相反的轉化,32位整數轉換成點分十進制
           

網絡位址初始化

初始化結構體

sockaddr_in

(上面提到過),在後面的代碼中在解釋。

每次建立伺服器端的套接字都輸入

IP

很麻煩,可以使用常數

INADDR_ANY

配置設定伺服器端的

IP

。對于多

IP

的伺服器,隻要端口号一緻,就可以從不同的

IP

位址接收資料。

向套接字配置設定網絡位址

把初始化的位址資訊配置設定給套接字。

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen); //成功時傳回0, 失敗時傳回-1

~ sockfd: 要配置設定位址資訊的套接字的檔案描述符
~ myaddr: 存有位址資訊的結構體變量位址值
~ addrlen: 結構體變量的長度
           
#include <sys/socket.h>

int listen(int sock, int backlog); 

~ sock: 希望進入等待連接配接請求狀态的套接字檔案描述符
~ backlog: 請求隊列的長度
           

受理用戶端連接配接請求

#include <sys/socket.h>

int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);// 成功時傳回建立的檔案描述符, 失敗傳回 -1

~ sock: 伺服器套接字檔案描述符
~ addr: 儲存發起連接配接請求的用戶端位址資訊的變量位址值,向這個位址的結構體寫入用戶端位址資訊
~ addrlen: 上述結構體的長度,函數向這個變量寫入值,是以使用指針
           
每次調用這個函數都會生成一個新的用戶端套接字。這是一個比較重要的概念。

請求連接配接的用戶端

#include <sys/socket.h>

int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen); // 成功傳回0, 失敗傳回-1

~ sock: 用戶端套接字檔案描述符
~ servaddr: 儲存目标伺服器端位址資訊的變量位址值
~ addrlen: 上述結構體的位元組長度
           
隻有在伺服器端接收連接配接請求或者網絡異常導緻的中斷才會使上述函數傳回

伺服器端完整代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void err_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock; //伺服器端套接字,用于“守門”
    int clnt_sock; //用戶端套接字,循環使用
    char message[BUF_SIZE]; //消息緩存
    int str_len, i;

    struct sockaddr_in serv_addr; //儲存伺服器端的位址資訊
    struct sockaddr_in clnt_addr; //儲存用戶端的位址資訊
    socklen_t clnt_addr_size;     //位址結構體的長度


    if(argc != )
    {
        printf("Usage : %s <port> \n", argv[]);
        exit();
    }


    serv_sock = socket(PF_INET, SOCK_STREAM, ); //配置設定套接字

    if(serv_sock == -)
        err_handling("socket() error");

    memset(&serv_addr, , sizeof(serv_addr)); //将伺服器端位址結構體全置為0,将sin_zero置為0
    serv_addr.sin_family = AF_INET; // 位址族
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地IP轉換為網絡位元組序
    serv_addr.sin_port = htons(atoi(argv[])); //綁定端口号

    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -) // 取serv_addr(sockaddr_in)的前部分代碼初始化 sockaddr 的部分,是一種有效且危險的轉換
        err_handling("shenmecuoel bind() error");

    if(listen(serv_sock, ) == -)
        err_handling("listen() error");

    clnt_addr_size = sizeof(clnt_addr);
    for(i=; i<; i++) //隻接受這麼多的連接配接請求
    {
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); //将接受到的請求的用戶端位址資訊寫入 clnt_addr
        if(clnt_sock == -) 
            err_handling("accept() error");
        else
            printf("Connected client. %d \n", i+);

        while((str_len = read(clnt_sock, message, BUF_SIZE)) != )
            write(clnt_sock, message, str_len); //回顯

        close(clnt_sock); //關閉用戶端套接字,檔案結束時候read傳回0
    }

    close(serv_sock);
    return ;
}

void err_handling(char * message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit();
}
           

用戶端代碼示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char * message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_addr;

    if(argc != )
    {
        printf("Usage : %s <IP> <port> \n", argv[]);
        exit();
    }

    sock = socket(PF_INET, SOCK_STREAM, );
    if(sock == -)
        error_handling("socket() error");

    memset(&serv_addr, , sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[]);
    serv_addr.sin_port = htons(atoi(argv[])); //從這裡網上與伺服器端相同,不再贅述

    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -) //向伺服器端發起請求,函數傳回也就是連接配接成功,向sock 寫入資訊也就是向伺服器發送資訊
        error_handling("connec() error");
    else
        puts("Connected ... ... ");
 while()
    {
        fputs("Inputs message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if(!strcmp(message, "Q\n"))
            break;

        str_len = write(sock, message, strlen(message));

        recv_len = ;
        while(recv_len < str_len) //知道發送了多少資料,接收的資料要相等
        {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - );
            if(recv_cnt == -) // 面向連接配接的流,不存在資料邊界
                error_handling("read() erroe");
            recv_len += recv_cnt;
        }

        message[recv_len] = ;
        printf("message from server : %s \n", message);
    }
    close(sock);
    return ;
}
void error_handling(char * message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit();
}
           

上述用戶端解決收到字元串資料時立即讀取并輸出。

TCP套接字中的 I/O 緩沖

write

函數調用後并非立即傳輸資料,

read

函數調用後并非馬上接受資料。

  • 每個套接字都存在輸入緩沖和輸出緩沖
  • 緩沖區在建立套接字時自動生成
  • 即使關閉套接字也會繼續傳輸輸出緩沖區中的資料
  • 關閉套接字将丢失輸入緩沖區中的資料