TCP/IP網絡編程系列之三(初級)

TCP/IP網絡編程系列之三-地址族與數據序列

分配給套接字的IP地址和端口

  IP是Internet Protocol (網絡協議)的簡寫,是爲首發網絡數據而分配給計算機的值。端口號並不是賦予計算機值,而是爲了區分程序中建立的套接字而分配給套接字的序號。linux

網絡地址

  網絡地址分爲IPV4和IPV6,分別你別爲4個字節地址簇和6個字節地址簇。IPV4標準的4個字節的地址分爲網絡地址和主機地址,且分爲A、B、C、D、E 等類型。通常不多用到E類型。以下圖所示:net-id指網絡ID,host-id指主機ID程序員

 

說明:編程

A類地址的首字節範圍是:0-127數組

B類地址的首字節範圍是:128-191網絡

C類地址的首字節範圍是:192-223socket

或者也能夠這樣分ide

A類地址的首位是以0開頭函數

B類地址的前兩位是以10開頭測試

C類地址的前三位是以110開頭ui

概述

網絡ID是爲區分網絡而設置的一部分IP地址。好比你向www.baidu.com公司傳輸數據,該公司內部構建了局域網,把全部的計算機鏈接起來。所以,首相向baidu.com網絡傳輸數據,也就是說,並不是一開始就瀏覽全部4字節的IP地址,進而找到目標主機;而是僅瀏覽4字節IP地址的網絡地址,先把數據傳到baidu.com的網絡。baidu.com網絡接收到數據以後,瀏覽傳輸數據的主機地址並將數據傳輸給目標地址。通常的網絡都會有路由器和交換機,因此其實是向路由器或交換機傳遞數據,由接收數據的路由器根據數據中的主機地址向目標主機傳送數據。

用於區分套接字的端口號

IP用於區分計算機,只要有IP地址就能想目標主機傳輸數據,可是僅憑這些數據沒法傳輸給最終的應用程序。假設在欣賞音樂的同時在聽音樂或者上網瀏覽網頁,這時至少須要1個接受視頻數據的套接字和1個接受網頁信息的套接字。可是如何區分它們呢,也就是說傳輸到計算機的網絡數據是發送給視頻播放器仍是音樂播放器?假設咱們開發了迅雷等應用程序,該程序用塊單位分割一個文件,從多臺計算機接受數據。那如何區分這些套接字呢?

計算機中通常都會有NIC(NetWork Interface Card,網絡接口卡)數據傳輸設備。經過NIC向計算機內部傳輸數據時會用到IP。OS負責把傳遞到內部的數據適當的分配給套接字,這時就要利用端口號。經過NIC接受的數據內有端口號,操做系統正是參考此端口號把數據傳輸給相應端口的套接字。端口號就是在同一操做系統內爲區分不一樣套接字而設置的,所以沒法將一個端口號分配給不一樣的套接字。而且,端口號由16位構成,可分配的端口號的範圍是0-65535,0-1023是知名端口號(Well-Know PORT),通常分配給特定應用程序,因此應當分配此範圍以外的值。端口號是不能重複,但TCP套接字和UDP套接字不會公用端口號,因此容許重複。好比:某TCP套接字使用9190端口號,則其餘TCP沒法就沒法使用端口號,可是UDP套接字就可使用。

總之,數據傳輸目標地址同時包含IP地址和端口號,只有這樣,數據纔會被傳輸到最總的目的應用程序(應用程序套接字)。

地址信息的表示

應用程序中使用的IP地址和端口號以結構體的形式給出了定義,咱們主要以IPV4爲中心。

struct sockaddr_in
{
      sa_family_t          sin_family;//地址族
      uint16_t              sin_port;//16位TCP/UDP端口號
      struct in_addr      sin_addr;//32位IP地址
      char                    sin_zero[8];//不使用
};
該結構體中的 in_addr用來存放32位的IP地址,定義以下
struct in_addr
{
    In_addr_t    s_addr;//32位IP地址
};

uint16_t in_addr_t等類型能夠參考POSIX,我這邊簡單說一下

POSIX中定義的數據類型
數據類型 數據類型說明 聲明的頭文件
int8_t signed 8-bit int sys/types.h
uint8_t unsigned 8-bit int  sys/types.h
int16_t signed 16-bit int sys/types.h
uint16_t unsigned 16-bit int  sys/types.h
int32_t signed  32-bit int 

sys/types.h

這主要是考慮到擴展性,若是使用int32_t類型的數據,就能保證在任什麼時候候都佔用4字節,即便未來用64位的來存儲也是同樣。

結構體sockaddr_in的成員分析

  • 成員sin_family

    每種協議族適用的地址族不一樣,好比IPV4使用4個字節地址族,IPV6使用16字節地址簇

      地址簇        含義

      AF_INET        IPV4網絡協議中使用的地址族

      AF_INET6      IPV6網絡協議中使用的地址族

      AF_LOCAL       本地通訊中採用的UNIX協議的地址族

  • 成員sin_port

    該成員保存16位端口號,具體在下面講解。

  • sin_addr

    保存32位的IP地址信息,且以網絡字節序保存,結構體in_addr聲明爲uint32_t,一次只須要保存32位整數類型便可。

  • sin_zero

    無特殊含義,只是未使用結構體sockaddr_in的大小與sockaddr結構體保持一致而插入的成員,必須填充爲0,不然沒法獲得想要的結果。以前在服務端bind函數的時候,

struct sockaddr_in serv_addr;

if(bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr)==-1),其中第二個參數,是由sockaddr_in結構體轉化而來的,而且但願獲得sockaddr結構體變量地址,包括地址簇、端口號、IP地址等。可是咱們直接向sockaddr結構體填充這些信息會帶來麻煩。

struct sockaddr

{

  sa_family_t   sin_family;//地址族

  char       sa_data[14];//地址信息

};

sa_data保存地址信息,包括IP地址和端口號,剩餘部分應填充爲0,這也是bind函數的要求。這對於包含地址信息來說很是麻煩,從而有了新的結構體sockaddr_in。

 網絡字節序和地址轉換

  • 字節序和網絡字節序

cpu向內存保存數據有兩種,這意味着cpu解析數據的方式也是兩種分別爲大端序和小端序。

  1. 大端序(Big Endian):高位字節存放到低位地址。
  2. 小端序(Little Endian):高位字節存放到高位地址。

好比0x00000001
大端序:內存低比特位 00000000 00000000 00000000 00000001 內存高比特位
小端序:內存低比特位 10000000 00000000 00000000 00000000 內存高比特位

還能夠以下圖表示:

因此出現了一個問題若是兩臺計算機的cpu的數據保存方式不一樣,可是他們是如何傳送數據的呢?如何進行網絡傳輸的呢?因此就規定了一個標準,在經過網絡傳輸的過程當中統一按照大端序。即先把數據數組轉化爲大端序格式而後進行傳輸。所以,全部計算機接受數據時應識別該數據的字節格式,小端序系統傳輸數據時應該轉化爲大端字節序的排列方式。

  • 字節序轉換(Endian Conversions)

因此咱們懂應該知道爲什麼在填充sockadr_in結構提早將數據轉化爲網絡字節序。轉換字節的函數:

unsigned short htons (unsigned short);

unsigned  short ntohs(unsigned  short);

unsigned  long htons (unsigned long);

unsigned  long ntohs (unsigned long);

經過函數名能夠知道他的功能,只要瞭解一下細節。htons中的h表明主機(host)字節序,n表明網絡字節序。s指的是short,l指的是long(linux 中long類型佔用4個字節),能夠解釋爲"把short類型數據從主機字節序轉化爲網絡字節序"。因此ntohs也就知道了吧。咱們經過例子來看一下效果:

運行結果以後會看到以下圖結果:

這就是小端序cpu運行結果,若是在大端值中是不會發生變化的。Intel和AMD系列的cpu都採用小端序標準。數據在傳輸過程當中須要通過轉換嗎?實際上沒有必要,這個過程是自動的。除了想sockaddr_in結構體變量填充數據外,其餘狀況不須要考慮字節序問題。

網絡地址的初始化與分配

sockaddr_in 中保存地址信息的成員爲32位整數型。所以,爲了分配IP地址將其轉化爲32位整數型數據。對咱們而言並不是易事。對於IP地址的的表示,咱們熟悉的是點分十進制,而非整型數據表示法。幸運的是,有個函數能夠幫咱們將字符串形式的IP地址轉化爲32位的整型數據。此函數在轉換類型的同時進行網絡字節序轉換。

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string)
成功時返回32位大端序整型數據,失敗時返回INADDR_NONE

下面是測試代碼:

運行結果以下圖所示:從運行結果能夠看出,inet_addr函數不只能夠把ip地址轉換爲32位整數,並且還能夠檢測無效的ip地址。而且輸出的確實是網絡字節序。還有一個函數與inet_addr函數功能徹底相同,只不過該函數利用了in_addr結構體,且使用頻率更高。

#include <arpa/inet.h>

int inet_aton(const char *string,struct in_addr *addr)
成功時返回1,失敗時返回0;

實際編程中若要調用inet_addr函數,須要將轉化後的IP地址信息代入sockaddr_in結構體中聲明的in_addr結構體變量。而inet_aton函數不須要此過程。緣由在於,若傳遞in_addr結構體變量地址值,函數會自動把結果填入該結構體變量。ok,下面再講解一個把網絡字節序整數型IP地址轉換成咱們熟悉的字符串形式。

#include <arpa/inet>

char *inet_ntoa(struct in_addr adr);
成功時返回轉換的字符串地址,失敗時返回-1。

但在調用時當心,返回值是char類型的指針。返回字符串地址意味着字符串已保存到內存空間,但該函數未向程序員要求分配內存,而是在內部申請了內存並保存了字符串。也就是說,調用完函數後,應該當即將字符串信息複製到其餘內存空間。由於在此調用該函數,則有可能覆蓋以前保存的字符串信息。總之,再次調用該函數前返回的字符串地址值是有效的。如要長期保存,則應將字符串複製到其餘內存空間。示例:

運行結果以下:

下面我把以前的代碼徹底從新組合一下。

服務端代碼:

 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 void ErrorMessage(char *message);
 9 
10 int main(int argc,char *argv[])
11 {
12     int serv_sock;
13     int client_sock;
14     struct sockaddr_in serv_addr;
15     struct sockaddr_in client_addr;
16     char *serverIP= "127.0.0.1";
17     char *servPort = "9190";
18     char message[]="Hi,TCPIP";
19     socklen_t clnt_addr_size;
20     serv_sock = socket(PF_INET,SOCK_STREAM,0);
21     if(serv_sock==-1)
22     {
23         ErrorMessage("Sock Error!");
24     }
25     memset(&serv_addr,0,sizeof(serv_addr));
26     serv_addr.sin_family=AF_INET;
27     serv_addr.sin_addr.s_addr=inet_addr(serverIP);
28     serv_addr.sin_port = htons(atoi(servPort));
29     if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
30     {
31         ErrorMessage("Bind() Error");
32     }
33     if(listen(serv_sock,5)==-1)
34     {
35         ErrorMessage("listen() error");
36     }
37     clnt_addr_size = sizeof(client_addr);
38     client_sock = accept(serv_sock,(struct sockaddr*)&client_addr,&clnt_addr_size);
39     if(client_sock==-1)
40     {
41         ErrorMessage("accept() error");
42     }
43     write(client_sock,message,sizeof(message));
44     close(client_sock);
45     close(serv_sock);
46     return 0;
47 }
48 void ErrorMessage(char *message)
49 {
50     fputs(message,stderr);
51     fputc('\n',stderr);
52     exit(1);
53 }

客戶端代碼:

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

void ErrorMessage(char *message);

int main(int argc,char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;
    char message[30];
    char *serv_port = "9190";
    int str_len;
    sock = socket(PF_INET,SOCK_STREAM,0);
    if(sock==-1)
    {
        ErrorMessage("socket() error");
    }
    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(serv_port));
    if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1)
    {
        ErrorMessage("connect() error");
    }
    str_len = read(sock,message,sizeof(message)-1);
    if(str_len==-1)
    {
        ErrorMessage("read() error");
    }
    printf("Message from server:%s\n",message);
    close(sock);
    return 0;
}
void ErrorMessage(char *message)
{
    fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);

}
Client

 運行結果以下圖所示:

相關文章
相關標籤/搜索