本篇博客主要記錄套接字編程API,從一些基本的API來一步一步了解套接字網絡編程。
大多數的套接字函數都以一個指向套接字地址結構的指針作為參數。每個協議簇都定義了自己的套接字地址結構。
套接字地址結構均以sockaddr_開頭,並以對應每個協議簇的唯一後綴結尾。
//ipv4的套接字地址結構
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;//32位的ipv4地址
};
//網絡套接字地址結構
struct sockaddr_in
{
//查看in.h源碼發現此處為__SOCKADDR_COMMON (sin_)
uint8_t sin_len //套接字地址結構的長度
sa_family_t sin_family // AF_INET
in_port_t sin_port; //端口號
struct in_addr sin_addr; //網絡地址
unsigned char sin_zero[8];//不使用,該字段必須為0
};
通常我們在使用套接字的時候,只會用到三個字段:sin_family,sin_addr和sin_port,如下:
struct sockaddr_in servaddr;//聲明一個套接字地址
bzero(&servaddr, sizeof(servaddr));//套接字結構體清0
servaddr.sin_family = AF_INET;//使用ipv4的協議簇
servaddr.sin_port = htons(13);//端口號,13為獲取日期和時間的端口
當作為一個參數傳遞進任何套接字函數時,套接字地址總是以引用形式來傳遞。
類似於void*代表通用指針類型,通用套接字地址結構可以便於參數傳遞,使得套接字函數能夠處理來自所支持的任何協議簇的套接字地址結構。
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
以bind函數為例,說明它的用法:
struct sockaddr_in servaddr;
//SA為struct scokaddr的簡寫
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));//必須對servaddr進行強制轉換
//附:Bind函數的原型
int bind(int , struct sockaddr* , socklen_t);
struct in6_addr
{
uint8_t __u6_addr8[16];//128位IPv6地址,網絡字節序
}
struct sockaddr_in6
{
uint8_t sin_len //套接字地址結構的長度,128
sa_family_t sin_family // AF_INET6
in_port_t sin6_port; //傳輸端口
uint32_t sin6_flowinfo; //IPv6 流標字段
struct in6_addr sin6_addr; //IPv6 地址
uint32_t sin6_scope_id; //對於具備范圍的地址,此字段標志其范圍
};
為IPV6套接字API定義的新的通用套接字地址結構,其足以容納所支持的任何套接字地址結構
書中說到在in.h文件中,可是我怎麼也找不到。後來在usr/include/X86_64-linux-gnu/bits的socket.h中找到
struct sockaddr_storage
{
uint8_t ss_len;//length of this struct
sa_family_t ss_family; // address family:AF_XXXX value
__ss_aligntype __ss_align; /* Force desired alignment. */
char __ss_padding[_SS_PADSIZE];//enough storage to hold any type of socket address that the system supports
};
當一個套接字函數傳遞一個套接字地址結構時,往往是采用引用的方式傳遞。為了讀取正確的字節數和寫入時不會越界,該套接字的長度也需要作為參數傳遞給套接字函數。
不過,其傳遞方式取決於該結構的傳遞方向:是從進程到內核,或者內核到進程
2.1 從進程到內核
這類函數有三個:bind,connect和sendto,其傳遞的時該結構的整數大小,這樣內核就知道從進程中復制多少數據,例如:
struct sockaddr_in serv;
connect( sockfd, ( SA * )&serv, sizeof( serv ) );//第三個參數為傳遞的該結構的整數大小
2.2 從內核到進程
這類函數有4個,accept,recvfrom,getsockname和getpeername,其傳遞的是指向某個套接字結構的指針和指向表示該結構大小的整數變量和指針:
struct sockaddr_un cli;
socklen_t len;
len = sizeof(cli);//len is value
getpeername(unixfd,(SA*)&cli,&len);//len既作為值被傳遞進去,又作為結果返回出來
字節序分為大端字節序和小端字節序,以下面的代碼來判斷系統到底時何種字節序:
#include "unp.h"
int main(int argc, char const *argv[])
{
union {
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
printf("%s: ", CPU_VENDOR_OS);
if (sizeof(short) == 2)
{
if (un.c[0]==0x01 && un.c[1]==0x02)
{
printf("big-endian\n");
}
else if(un.c[0]==0x02 && un.c[1]==0x01)
{
printf("little-endian\n");
}
else
printf("unknown\n");
}
else
printf("sizeof(short)=%d\n", (int)sizeof(short));
exit(0);
return 0;
}
運行此代碼後輸出:
$ gcc byteorder.c -o byteorder -lunp
$ ./byteorder
x86_64-unknown-linux-gnu: little-endian
代表本機時小端字節序
網絡字節序使用大端字節序來傳送,那麼本機和網絡之間要正確傳遞數據,就需要一個轉換函數。
這兩種字節序之間的轉換使用以下4種函數:
uint16_t htons( uint16_t host16bitvalue );
uint32_t htonl( uint32_t host32bitvalue );----均返回:網絡字節序的值
uint16_t ntohs( uint16_t net16bitvalue );
uint32_t ntohl( uint32_t net32bitvalue );----均返回:主機字節序的值
h代表host主機,n代表net網絡,s代表short,l代表long
測試用例:將0x1234(4660)從主機字節序轉換成網絡字節序,轉換後應該為0x3412(13330)
#include "unp.h"
int main(int argc, char const *argv[])
{
uint16_t portAddr;
portAddr = htons(0x1234);//十進制值為4660
printf("the port 0x1234 netword port is:%d\n", portAddr);
printf("the port is:%d\n", ntohs(portAddr));
return 0;
}
//輸出結果
the port 0x1234 netword port is:13330
the port is:4660//答案正確
操作多字節字段的函數有兩組,他們既不對數據作解釋,也不假設數據是以空字符結束的C字符串。
#include
//b開頭(表示字節)的一組函數
void bzero(void* dest,size_t nbytes);//將指針dest以後的nbytes位置0
void bcopy(const void* src,void *dest , size_t nbytes);//將指針src後的nbytes位復制到指針dest
int bcmp(const void *ptr1, const void* ptr2, size_t nbytes);//比較ptr1和ptr2後的nbytes位的大小
//mem開頭(表示內存)的一組函數
void *memset(void *dest , int c , size_t len);//將dest開始的一段長度為len的內存的值設為c
void *memcpy(void *dest , const void* src , size_t nbytes);//同bcopy
int memcmp(const void *ptr1 , const void *ptr2, size_t nbytes);//同bcmp
在點分十進制數串和與它長度為32位的網絡字節序二進制值間轉換ipv4地址
//將__cp所指的字符串轉換成一個32位的網絡字節序,保存在__inp結構體中
int inet_aton (const char *__cp, struct in_addr *__inp) ;//返回:若字符串有效則為1,否則為0
//同上,只不過轉換後的值作為返回值直接返回
in_addr_t inet_addr (const char *__cp);//如字符串有效則返回32位字節序,否則返回INADDR_NONE
//將一個32位的網絡字節序二進制IPv4地址轉換成相應的點分十進制數串
char *inet_ntoa (struct in_addr __in);//返回一個點分十進制數串的指針
測試用例:輸入一個點分十進制網絡地址,測試輸出
#include "unp.h"
int main(int argc, char const *argv[])
{
struct in_addr addr;
char *pAddr;
inet_aton(argv[1],&addr);
printf("%d\n",addr.s_addr);
pAddr = inet_ntoa(addr);
printf("%s\n",pAddr);
return 0;
}
//輸出結果:
$ ./Test_inet_aton 127.0.0.1 //0x7F000001
16777343//小端字節序,0100007F
127.0.0.1//轉換為點分十進制數串
隨著IPv6出現的新函數,p代表表達式,即ASCII字符串;n代表數值,即存放在套接字地址結構中的二進制值。
這兩個函數對於ipv4和ipv6都適用。
//int __af為AF_INET或者AF_INET6
//將__cp存儲的字符串轉換成二進制,結果存放在__buf
int inet_pton (int __af, const char *__cp, void * __buf)
//將__cp所指的二進制轉換成字符串,__len指定二進制大小,
const char *inet_ntop (int __af, const void *__cp, char *__buf, socklen_t __len)
inet_ntop函數的基本問題時要求調用者傳遞一個指向某個二進制地址的指針,而該地址通常包含在一個套接字地址中
因此調用者事先需要知道這個結構的格式。
struct sockaddr_in addr;//需要事先定義,如果為ipv6則為sockaddr_in6
inet_ntop(AF_INET,&addr.sin_addr,str,sizeof(str));
為了解決這個問題,作者自己寫了一個函數:
#include "unp.h"
char *sock_ntop(const struct sockaddr *sa, socklen_t salen);
//具體實現如下:
char *
sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
char portstr[8];
static char str[128]; /* Unix domain is largest */
switch (sa->sa_family) {
case AF_INET: {
struct sockaddr_in *sin = (struct sockaddr_in *) sa;
if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
return(NULL);
if (ntohs(sin->sin_port) != 0) {
snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
strcat(str, portstr);
}
return(str);
}
/* end sock_ntop */
#ifdef IPV6
case AF_INET6: {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *) sa;
str[0] = '[';
if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
return(NULL);
if (ntohs(sin6->sin6_port) != 0) {
snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
strcat(str, portstr);
return(str);
}
return (str + 1);
}
#endif
//後續省略了一些case,如case AF_UNIX:
}
字節流套接字上調用read和write輸入和輸出的字節數可能比請求的數量要少,所以作者自己寫了readn,writen和readline三個函數。
readn的實現如下:
#include "unp.h"
ssize_t /* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {//循環讀取
if (errno == EINTR)
nread = 0; /* and call read() again */
else
return(-1);
} else if (nread == 0)
break; /* EOF */
nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
/* end readn */
writen的實現如下:
#include "unp.h"
ssize_t /* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0; /* and call write() again */
else
return(-1); /* error */
}
nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
readline的實現如下:
#include "unp.h"
//較慢的一個版本,需要每次都調用系統的read函數
//作者還寫了一個較快的版本,這裡就不貼出代碼了,主要思想是定義一個my_read一次讀取進來,但每次都只返回給readline一個字符
ssize_t
readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = read(fd, &c, 1)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
if (n == 1)
return(0); /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return(-1); /* error */
}
*ptr = 0;
return(n);
}
/* end readline */