程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C/C++ 網絡編程3: 套接字基礎

C/C++ 網絡編程3: 套接字基礎

編輯:關於C++
部分信息參考 中國石油大學 信息安全實驗 信息安全實驗四實驗參考 參考 信息安全實驗資料 四個PPT文件 server.c

套接字地址

Linux系統的套接字可以支持多種協議,每種不同的協議都是用不同的地址結構。 在頭文件中定義了一個通用套接字地址結構sockaddr:
struct sockaddr
{
    unsigned short sa_family; //16位 套接字的協議簇地址類型,AF_XX
    char        sa_data[14];//14字節 存儲具體的協議地址
};
為了處理struct sockaddr,程序員創造了一個並列的大小相同結構:struct sockaddr_in(“in”代表”Internet”。) struct sockaddr_in在/usr/include/netinet/in.h中定義:
struct sockaddr_in
{
    unsigned short sin_len;  //IPv4地址長度
    short int sin_family;   //16位 指代協議簇,TCP套接字編程為AF_INET
    unsigned short sin_port;     //16位端口號(使用網絡字節順序),數據類型是一個16位的無符號整數
    struct in_addr sin_addr;   //32位,存儲IP地址,是一個in_addr結構體
    unsigned char sin_zero[8];     //8字節,為了讓sockaddr與sockaddr_in兩個數據結構保持大小相同而保留的空字節
};
struct in_addr //32 位
{
    unsigned long s_addr;  //按照網絡字節順序存儲IP地址
};
填充特定協議地址時使用sockaddr_in 作為bind()、connect()、sendto()、recvfrom()等函數的參數時需要使用sockaddr, 這時要通過指針強制轉換的方式轉為struct sockaddr 指針。

IPv4地址結構示例

struct sockaddr_in mysock;
mysock.sin_family = AF_INET;  //TCP地址結構
mysock.sin_port = htons(3333); //字節順序轉換函數
mysock.sin_addr.s_addr = inet_addr("166.111.160.10"); //設置IP地址
//如果mysock.sin_addr.s_addr = INADDR_ANY,則不指定IP地址(用於server程序)
bzero(&(mysock.sin_zero),8); //設置sin_zero為8位保留字節

IPV6套接字地址結構sockaddr_in6

#DEFINE SIN6_LEN
struct sockaddr_in6
{
    unsigned short  sin6_len;        //16位 IPv6地址長度,是一個無符號的8位整數,表示128位的IPv6地址
    short int   sin6_family;   //16位 地址類型為AF_INET6
    unsigned short  sin6_port;     //16位 存儲端口號,使用網絡字節順序
    unsigned short int sin6_flowinfo;  //低24位是流量標號,然後是4位優先級標志,剩下4位保留
    struct in6_addr sin6_addr;   //IPv6地址,網絡字節順序
};
struct in6_addr
{
    unsigned long   s6_addr;  //網絡字節順序的IPv6地址
};

IP地址轉換函數

inet_aton():將字符串形式的IP地址轉換成二進制形式的IP地址,成功返回1,否則返回0,轉換後的IP地址存儲在參數inp中。 inet_ntoa():將32位二進制形式的IP地址轉換為數字點形式的IP地址,結果在函數返回值中返回。
unsigned long inet_aton(const char *cp, struct in_addr *inp);
char* inet_ntoa(struct in_addr in);

網絡字節順序

字節序,顧名思義字節的順序,就是大於一個字節的數據在內存中的存放順序。 在跨平台以及網絡程序應用中字節序才是一個應該被考慮的問題。 網絡字節序是TCP/IP規定的一種數據表示格式,與具體的CPU類型、操作系統無關,從而可以保證數據在不同主機之間傳輸時能被正確解釋。網絡字節順序采用big endian(大端字節序)。 Intel x86系列CPU使用的都是little endian(小端字節序) 大端字節序(big-endian):低地址存放最高有效字節 小端字節序(little-endian):低地址存放最低有效字節 例如數字0x12345678(DWORD)在兩種不同字節序CPU中的存儲順序如下所示:

字節順序轉換函數

下面四個函數分別用於長整型和短整型數在網絡字節序和主機字節序之間進行轉換,其中s指short,l指long,h指host,n指network
#include 
unsigned long htonl(unsigned long host_long);
unsigned short htons(unsigned short host_short);
unsigned long ntohl(unsigned long net_long);
unsigned short ntohs(unsigned short net_short);

什麼時候要考慮字節序問題

如果是應用層的數據,即對TCP/IP來說是透明的數據,不用考慮字節序的問題。因為接收端收到的順序是和發送端一致的 但對於TCP/IP的IP地址、端口號來說就不一樣了,例如
unsigned short prot = 0x0012  //十進制18
struct sockaddr_in mysock;
mysock.sin_family = AF_INET;  //TCP地址結構
mysock.sin_port = prot;
因為網絡字節序是big endian,即低地址存放的是數值的高位,所以TCP/IP實際上把這個port解釋為0x1200(十進制4608)。 本來打算是要在端口18建立連接的,但TCP/IP協議棧卻在端口4608建立了連接

套接字的工作原理

INET 套接字就是支持 Internet 地址族的套接字,它位於TCP協議之上,BSD套接字之下, 如圖所示,這裡也體現了Linux網絡模塊分層的設計思想(圖在PPT裡,自己想象吧…) INET和 BSD 套接字之間的接口通過 Internet 地址族套接字操作集實現,這些操作集實際是一組協議的操作例程, 在include/linux/net.h中定義為proto_ops:
struct proto_ops {
    int family; 
    int (*release) (struct socket *sock);
    int (*bind) (struct socket *sock, struct sockaddr *umyaddr,   int sockaddr_len);
    int (*connect) (struct socket *sock, struct sockaddr *uservaddr, int sockaddr_len, int flags);
    int (*socketpair) (struct socket *sock1, struct socket *sock2);
    int (*accept) (struct socket *sock, struct socket *newsock, int flags);
    int (*getname) (struct socket *sock, struct sockaddr *uaddr, int *usockaddr_len, int peer);
    unsigned int (*poll) (struct file *file, struct socket *sock, struct poll_table_struct *wait);
    int (*ioctl) (struct socket *sock, unsigned int cmd, unsigned long arg);
    int (*listen) (struct socket *sock, int len);
    int (*shutdown) (struct socket *sock, int flags);
    int (*setsockopt) (struct socket *sock, int level, int optname, char *optval, int optlen);
    int (*getsockopt) (struct socket *sock, int level, int optname, char *optval, int *optlen);
    int (*sendmsg) (struct socket *sock, struct msghdr *m, int  total_len, struct scm_cookie *scm);
    int (*recvmsg) (struct socket *sock, struct msghdr *m, int total_len, int flags, struct scm_cookie *scm);
    int (*mmap) (struct file *file, struct socket *sock, struct vm_area_struct * vma);
    ssize_t (*sendpage) (struct socket *sock, struct page *page, int offset, size_t size, int flags);
};
這個操作集類似於文件系統中的file_operations結構。BSD套接字層通過調用proto_ops 結構中的相應函數執行任務。 BSD套接字層向 INET 套接字層傳遞socket數據結構來代表一個BSD套接字,

socket 定義

socket結構在include/linux/net.h中定義:
struct socket {
   socket_state state;
   unsigned long flags;
   struct proto_ops *ops;
   struct inode *inode;
   struct fasync_struct *fasync_list; /* Asynchronous wake up list */
   struct file *file;       /* File back pointer for gc */
   struct sock *sk;
   wait_queue_head_t wait;
   short type;
   unsigned char passcred;
};
但在INET套接字層中,它利用自己的sock數據結構來代表該套接字,因此,這兩個結構之間存在著鏈接關系 Sock結構定義於include/net/sock.h(此結構有80多行,在此不予列出 在BSD的socket數據結構中存在一個指向sock的指針sk,而在sock中又有一個指向socket的指針, 這兩個指針將BSD socket數據結構和sock數據結構鏈接了起來。 通過這種鏈接關系,套接字調用就可以方便地檢索到sock數據結構 實際上,sock數據結構可適用於不同的地址族,它也定義有自己的協議操作集proto 進程在利用套接字進行通訊時,采用客戶-服務器模型。服務器首先創建一個套接字,並將某個名稱綁定到該套接字上,套接字的名稱依賴於套接字的底層地址族,但通常是服務器的本地地址

/etc/services 文件

對於INET套接字來說,服務器的地址由兩部分組成:服務器的IP地址和服務器的端口地址。已注冊的標准端口可查看/etc/services 文件 將地址綁定到套接字之後,服務器就可以監聽請求連接該綁定地址的傳入連接 連接請求由客戶生成,它首先建立一個套接字,並指定服務器的目標地址以請求建立連接 傳入的連接請求通過不同的協議層到達服務器的監聽套接字 服務器接收到傳入請求後,如果能夠接受該請求,服務器必須創建一個新的套接字來接受該請求並建立通信連接(用於監聽的套接字不能用來建立通信連接),這時,服務器和客戶就可以利用建立好的通信連接傳輸數據 BSD套接字上的詳細操作與具體的底層地址族有關,底層地址族的不同實際意味著尋址方式、采用的協議等的不同 Linux 利用BSD套接字層抽象了不同的套接字接口。在內核的初始化階段,內建於內核的不同地址族分別以BSD套接字接口在內核中注冊 然後,隨著應用程序創建並使用BSD套接字 內核負責在BSD套接字和底層的地址族之間建立聯系。這種聯系通過交叉鏈接數據結構以及地址族專有的支持例程表建立 在內核中,地址族和協議信息保存在inet_protos向量中,其定義於include/net/protocol.h
struct inet_protocol *inet_protos[MAX_INET_PROTOS];
/* This is used to register protocols. */
struct inet_protocol {
    int   (*handler)(struct sk_buff *skb);
    void  (*err_handler)(struct sk_buff *skb, u32 info);
    struct inet_protocol    *next;
    unsigned char protocol;
    unsigned char copy:1;
    void      *data;
    const char    *name;
};

建立套接字

Linux在利用socket()系統調用建立新的套接字時,需要傳遞套接字的地址族標識符、套接字類型以及協議,其函數定義於net/socket.c中
asmlinkage long sys_socket(int family, int type, int protocol) {
  int retval;
  struct socket *sock;
  retval = sock_create(family, type, protocol, &sock);
  if (retval < 0)
     goto out;
  retval = sock_map_fd(sock);
  if (retval < 0)
     goto out_release;
 out:
      /* It may be already another descriptor 8) Not kernel problem. */
     return retval;
 out_release:
     sock_release(sock);
     return retval;
}

sockfs

實際上,套接字對於用戶程序而言就是特殊的已打開的文件。內核中為套接字定義了一種特殊的文件類型,形成一種特殊的文件系統sockfs 所謂創建一個套接字,就是在sockfs文件系統中創建一個特殊文件,或者說一個節點,並建立起為實現套接字功能所需的一整套數據結構 所以,函數sock_create()首先是建立一個socket數據結構,然後將其“映射”到一個已打開的文件中,進行socket結構和sock結構的分配和初始化 實際上,socket結構與sock結構是同一事物的兩個方面。如果說socket結構是面向進程和系統調用界面的,那麼sock結構就是面向底層驅動程序的 把與文件系統關系比較密切的那一部分放在socket結構中,把與通信關系比較密切的那一部分則單獨組成一個數據結構,即sock結構 由於這兩部分數據在邏輯上本來就是一體的,所以要通過指針互相指向對方,形成一對一的關系

在INET BSD套接字上綁定(bind)地址

為了監聽傳入的Internet 連接請求,每個服務器都需要建立一個INET BSD套接字,並且將自己的地址綁定到該套接字 將地址綁定到某個套接字上之後,該套接字就不能用來進行任何其他的通信,因此,該socket數據結構的狀態必須為TCP_CLOSE 傳遞到綁定操作的sockaddr數據結構中包含要綁定的 IP地址以及一個可選的端口地址。被綁定的IP地址保存在sock數據結構的rcv_saddr和 saddr域中,這兩個域分別用於哈希查找和發送用的IP地址。 端口地址是可選的,如果沒有指定,底層的支持網絡會選擇一個空閒的端口 當底層網絡設備接受到數據包時,它必須將數據包傳遞到正確的 INET 和 BSD 套接字以便進行處理,因此,TCP維護多個哈希表,用來查找傳入 IP 消息的地址,並將它們定向到正確的socket/sock 對 TCP 並不在綁定過程中將綁定的sock數據結構添加到哈希表中,在這一過程中,它僅僅判斷所請求的端口號當前是否正在使用。在監聽操作中,該 sock 結構才被添加到 TCP 的哈希表中

在INET BSD套接字上建立連接 (connect)

創建一個套接字之後,該套接字不僅可以用於監聽入站的連接請求,也可以用於建立出站的連接請求。不論怎樣都涉及到一個重要的過程:建立兩個應用程序之間的虛擬電路。出站連接只能建立在處於正確狀態的 INET BSD 套接字上, 因此,不能建立於已建立連接的套接字,也不能建立於用於監聽入站連接的套接字。也就是說,該 BSD socket 數據結構的狀態必須為 SS_UNCONNECTED 在建立連接過程中,雙方 TCP 要進行三次“握手”。如果 TCP sock 正在等待傳入消息,則該 sock 結構添加到 tcp_listening_hash 表中,這樣,傳入的 TCP 消息就可以定向到該 sock 數據結構

監聽(listen) INET BSD 套接字

當某個套接字被綁定了地址之後,該套接字就可以用來監聽專屬於該綁定地址的傳入連接。網絡應用程序也可以在未綁定地址之前監聽套接字,這時,INET 套接字層將利用空閒的端口編號並自動綁定到該套接字。套接字的監聽函數將 socket 的狀態改變為TCP_LISTEN 當接收到某個傳入的 TCP 連接請求時,TCP 建立一個新的 sock 數據結構來描述該連接。當該連接最終被接受時,新的 sock 數據結構將變成該 TCP 連接的內核bottom_half部分,這時,它要克隆包含連接請求的傳入 sk_buff 中的信息,並在監聽 sock 數據結構的 receive_queue 隊列中將克隆的信息排隊。克隆的 sk_buff 中包含有指向新 sock 數據結構的指針

接受連接請求(accept)

接受操作在監聽套接字上進行,從監聽 socket 中克隆一個新的 socket 數據結構。其過程如下: 接受操作首先傳遞到支持協議層,即INET中,以便接受任何傳入的連接請求。接受操作可以是阻塞的或是非阻塞的。非阻塞時,若沒有可接受的傳入連接,則接受操作將失敗,而新建立的socket數據結構被拋棄。阻塞時,執行阻塞操作的網絡應用程序將添加到等待隊列中並保持掛起直到接收到一個TCP連接請求為止。 當連接請求到達之後,包含連接請求的sk_buff被丟棄,而由TCP建立的新sock數據結構返回到INET套接字層,在這裡,sock數據結構和先前建立的新socket數據結構建立鏈接。而新socket的文件描述符(fd)被返回到網絡應用程序,此後,應用程序就可以利用該文件描述符在新建立的INET BSD套接字上進行套接字操作

套接字為用戶提供的系統調用

見PPT 3.7

getsockname()函數

getsockname(): 獲取與當前套接字綁定的IP地址及端口
#include 
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回0,失敗返回-1,並在errno中設置錯誤代碼。 錯誤代碼:
EBADF :
The argument sockfd is not a valid descriptor.
EFAULT :
The addr argument points to memory not in a valid part of the process address space.
EINVAL :
addrlen is invalid (e.g., is negative).
ENOBUFS :
Insufficient resources were available in the system to perform the operation.
ENOTSOCK :
The argument sockfd is a file, not a socket.

getpeername()函數

#include 
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回0,失敗返回-1,並在errno中設置錯誤代碼。 錯誤代碼:
EBADF
The argument sockfd is not a valid descriptor.
EFAULT
The addr argument points to memory not in a valid part of the process address space.
EINVAL
addrlen is invalid (e.g., is negative).
ENOBUFS
Insufficient resources were available in the system to perform the operation.
ENOTCONN
The socket is not connected.
ENOTSOCK
The argument sockfd is a file, not a socket.

gethostbyname()和gethostbyaddr()

gethostbyname():主機名轉換為IP地址 gethostbyaddr():IP地址轉換成主機名
#include 
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
struct hostent {
       char    *h_name;           /* 主機的正式名稱 */
       char    **h_aliases;       /* 主機別名列表 */
       int       h_addrtype;        /* 主機地址類型 */
       int       h_length;            /* 地址長度 */
       char    **h_addr_list;    /* 地址列表 */
};
#define     h_addr h_addr_list[0] /* 保持後向兼容 */

getservbyname()和getservbyport()

getservbyname():根據給定名字查找相應服務,返回服務的端口號 getservbyport():給定端口號和可選協議查找相應服務
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
struct servent {
    char *s_name; /* official service name */
    char **s_aliases; /* alias list */
    int s_port; /* port number */
    char *s_proto; /* protocol to use */
}

字節處理函數 bzero bcopy bcmp memset memcpy memcmp

套接字地址是多字節數據,不是以空字符結尾的,Linux提供兩組函數來處理多字節數據。一組函數以b開頭,適合BSD系統兼容的函數;另一組函數以mem開頭,是ANSI C提供的函數
#include
void bzero(void *s,int n);
void bcopy(const void *src,void *dest,int n);
void bcmp(const void *s1,const void *s2,int n);
void *memset(void *s,int c,size_t n);
void *memcpy(void *dest,void *src,size_t n);
void memcmp(const void *s1, const void *s2,size_t n);
函數bzero將參數s指定的內容的前n個字節設置為0,通常用它來將套接字地址清零 函數bcopy從參數src指定的內存區域拷貝指定數目的字節內容到參數dest指定的內存區域 函數bcmp比較參數s1指定的內存區域和參數s2指定的內存區域的前n個字節內容,相同則返回0,否則返回非0 將參數s指定的內存區域的前n個字節設置為參數c的內容 類似於bcopy,但bcopy能處理參數src和參數dest所指定的區域有重疊的情況,而memcpy不能 比較參數s1和參數s2指定區域的前n個字節內容,相同則返回0,否則返回非0

小結

套接字標識TCP/IP的連接 使用套接字要注意: 1、sockaddr與sockaddr_in的區別 2、網絡字節順序 了解套接字的工作原理 掌握套接字的通信過程
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved