客戶與服務器通信使用TCP在同一網絡通信時,大致按下面的方式通信:client→TCP→IP→以太網驅動程序→以太網→以太網驅動程序→IP→TCP→server。若不在同一網絡則需要路由器連接。
客戶端程序解析:
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
#include <stdio.h> #include "unp.h"
int main(int argc, char **argv){ int socketfd=-1, n, inet, con; char reciveline[MAXLINE + 1]; struct sockaddr_in servaddr; if (argc != 2){ printf("usage: a.out <IPaddress>\n"); return 0; } if( (socketfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ printf("socket error\n"); return 0; } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(2329); /* daytime server */ if ((inet = inet_pton(AF_INET, argv[1], &servaddr.sin_addr)) <= 0){ printf("inet_pton error for %s\n", argv[1]); return 0; } if ((con = connect(socketfd, (SA *) &servaddr, sizeof(servaddr))) < 0){ printf("connect error\n"); return 0; } while ( (n = read(socketfd, reciveline, MAXLINE)) > 0) { reciveline[n] = 0; /* null terminate */ if (fputs(reciveline, stdout) == EOF){ printf("fputs error\n"); return 0; } } if (n < 0){ printf("read error\n"); return 0; } return 0; } |
上面是一個簡單客戶端程序,通過這個程序可以知道客戶端在通信時的過程,下面我們詳細解析:
sockaddr_in
(在netinet/in.h中定義):
2 3 4 5 6 7 8 9 10 11 12 |
struct sockaddr_in
{ short sin_family;/*Address family一般來說AF_INET(地址族)PF_INET(協議族)*/ unsigned short sin_port;/*Port number(必須要采用網絡數據格式,普通數字可以用htons()函數轉換成網絡數據格式的數字)*/ struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/ unsigned char sin_zero[8];/*Same size as struct sockaddr沒有實際意義,只是為了 跟SOCKADDR結構在內存中對齊*/ }; |
(在ws2def.h中定義):
1 2 3 4 5 6 7 8 9 10 11 |
struct sockaddr_in { #if(_WIN32_WINNT<0x0600) short sin_family; #else//(_WIN32_WINNT<0x0600) address_family sin_family; #endif//(_WIN32_WINNT<0x0600) ushort sin_port; in_addr sin_addr; char sin_zero[8]; } |
(在WinSock2.h中定義):
1 2 3 4 5 6 |
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; |
在linux下:
in_addr結構
1 2 3 4 5 |
typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; |
在windows下:
1 2 3 4 5 6 7 8 |
typedef struct in_addr { union{ struct { unsigned char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { unsigned short s_w1,s_w2; } S_un_w; unsigned long S_addr; }S_un; }in_addr; |
int socket(int domain, int type, int protocol);
即創建一個socket,即套接字。就是對通信端點的抽象。返回套接字描述符,就如程序通過文件描述符訪問文件一樣,套接字描述符是訪問套接字的一種路徑。從某種意義上說,套接字也在文件,所以許多對文件描述符使用的函數,對套接字描述符同樣適用,但是有些是不可使用的:參數說明如下:
1、在參數表中,domain指定使用何種的地址類型,比較常用的有:
PF_INET, AF_INET: Ipv4網絡協議;
PF_INET6, AF_INET6: Ipv6網絡協議。
2、type參數的作用是設置通信的協議類型,可能的取值如下所示:
SOCK_STREAM: 提供面向連接的穩定數據傳輸,即TCP協議。
OOB: 在所有數據傳送前必須使用connect()來建立連接狀態。
SOCK_DGRAM: 使用不連續不可靠的數據包連接。
SOCK_SEQPACKET: 提供連續可靠的數據包連接。
SOCK_RAW: 提供原始網絡協議存取。
SOCK_RDM: 提供可靠的數據包連接。
SOCK_PACKET: 與網絡驅動程序直接通信。
3、參數protocol用來指定socket所使用的傳輸協議編號。這一參數通常不具體設置,一般設置為0即可。
該函數如果調用成功就返回新創建的套接字的描述符,如果失敗就返回INVALID_SOCKET。套接字描述符是一個整數類型的值。每個進程的進程空間里都有一個套接字描述符表,該表中存放着套接字描述符和套接字數據結構的對應關系。該表中有一個字段存放新創建的套接字的描述符,另一個字段存放套接字數據結構的地址,因此根據套接字描述符就可以找到其對應的套接字數據結構。每個進程在自己的進程空間里都有一個套接字描述符表但是套接字數據結構都是在操作系統的內核緩沖里。
extern void bzero(void *s, int n);
該函數在#include <string.h>,功能是置字節字符串s的前n個字節為零,該函數bzero無返回值。在這里就是給套接字清零,也就是從&servaddr指針所指的地址位置開始,將sizeof(sevaddr)字節置為0,有的實現就是用函數:memset(&servaddr,0x00,sizeof(sevaddr));
AF_INET & AF_INET6 & AF_UNIX
AF_INET(又稱 PF_INET)是 IPv4 網絡協議的套接字類型,AF_INET6 則是 IPv6 的;而 AF_UNIX 則是 Unix 系統本地通信。
選擇 AF_INET 的目的就是使用 IPv4 進行通信。因為 IPv4 使用 32 位地址,相比 IPv6 的 128 位來說,計算更快,便於用於局域網通信。而且 AF_INET 相比 AF_UNIX 更具通用性,因為 Windows 上有 AF_INET 而沒有 AF_UNIX。
htons(), ntohl(), ntohs(),htons()
在C/C++寫網絡程序的時候,往往會遇到字節的網絡順序和主機順序的問題。這是就可能用到htons(), ntohl(), ntohs(),htons()這4個函數。
網絡字節順序與本地字節順序之間的轉換函數:
htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"
之所以需要這些函數是因為計算機數據表示存在兩種字節順序:NBO與HBO
網絡字節順序NBO(Network Byte Order):按從高到低的順序存儲,在網絡上使用統一的網絡字節順序,可以避免兼容性問題。
主機字節順序(HBO,Host Byte Order):不同的機器HBO不相同,與CPU設計有關,數據的順序是由cpu決定的,而與操作系統無關。如 Intelx86結構下,short型數0x1234表示為34 12, int型數0x12345678表示為78 56 34 12如IBM power PC結構下,short型數0x1234表示為12 34, int型數0x12345678表示為12 34 56 78
由於這個原因不同體系結構的機器之間無法通信,所以要轉換成一種約定的數序,也就是網絡字節順序,其實就是如同power pc那樣的順序。在PC開發中有ntohl和htonl函數可以用來進行網絡字節和主機字節的轉換。
在Linux和Windows網絡編程時需要用到htons和htonl函數,用來將主機字節順序轉換為網絡字節順序。
在Intel機器下,執行以下程序
int main()
{
printf("%d \n",htons(16));
return 0;
}
得到的結果是4096,初一看感覺很怪。
解釋如下,數字16的16進制表示為0x0010,數字4096的16進制表示為0x1000。由於Intel機器是小尾端,存儲數字16時實際順序為1000,存儲4096時實際順序為0010。因此在發送網絡包時為了報文中數據為0010,需要經過htons進行字節轉換。如果用IBM等大尾端機器,則沒有這種字節順序轉換,但為了程序的可移植性,也最好用這個函數。另外用注意,數字所占位數小於或等於一個字節(8 bits)時,不要用htons轉換。這是因為對於主機來說,大小尾端的最小單位為字節(byte)。
inet_pton和inet_ntop函數
Linux下這2個IP地址轉換函數,可以在將IP地址在“點分十進制”和“整數”之間轉換,而且,inet_pton和inet_ntop這2個函數能夠處理ipv4和ipv6。算是比較新的函數了。
inet_pton函數原型如下[將“點分十進制”→“整數”]
int inet_pton(int af, const char *src, void *dst);
這個函數轉換字符串到網絡地址,第一個參數af是地址族,轉換后存在dst中,inet_pton 是inet_addr的擴展,支持的多地址族有下列:
af = AF_INET
Src為指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函數將該地址轉換為in_addr的結構體,並復制在*dst中
af =AF_INET6
src為指向IPV6的地址,,函數將該地址轉換為in6_addr的結構體,並復制在*dst中
如果函數出錯將返回一個負值,並將errno設置為EAFNOSUPPORT,如果參數af指定的地址族和src格式不對,函數將返回0。
inet_ntop函數原型如下[將“點分十進制”→“整數”]
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
這個函數轉換網絡二進制結構到ASCII類型的地址,參數的作用和上面相同,只是多了一個參數socklen_t cnt,他是所指向緩存區dst的大小,避免溢出,如果緩存區太小無法存儲地址的值,則返回一個空指針,並將errno置為ENOSPC。
connect()
用來將參數sockfd 的socket 連至參數serv_addr 指定的網絡地址. 結構sockaddr請參考bind(). 參數addrlen 為sockaddr 的結構長度.
返回值:成功則返回0, 失敗返回-1, 錯誤原因存於errno 中
定義在#include <sys/socket.h>
函數原型: int connect(int s, const struct sockaddr * name, int namelen);
參數:
s:標識一個未連接socket
name:指向要連接套接字的sockaddr結構體的指針
namelen:sockaddr結構體的字節長度
ssize_t read(int fd,void *buf,size_t nbyte)
read()會把參數fd所指的文件傳送nbyte個字節到buf指針所指的內存中。read函數是負責從fd中讀取內容.當讀成功 時,read返回實際所讀的字節數,如果返回的值是0 表示已經讀到文件的結束了,小於0表示出現了錯誤.如果錯誤為EINTR說明讀是由中斷引起 的,如果是ECONNREST表示網絡連接出了問題。
int fputs(const char *str, FILE *stream)
參數
str:這是一個數組,包含null結尾的要寫入的字符序列。
stream:這是一個文件對象的標識字符串將被寫入流的指針。
返回值:這個函數返回一個非負的值,否則,錯誤返回EOF。
有了上面這些概念,這段代碼就比較容易看懂了,即,指定地址類型、協議類型,協議編號創建socket,返回套接口描述字,然后清空地址緩存,指定地址族和端口號,將輸入地址轉換為網絡地址,然后與服務端建立鏈接,把數據讀入緩存區並輸出到標准輸出。
服務端程序解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include "unp.h" #include <time.h>
int main(int argc, char **argv){ int listenfd, connfd; struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks;
listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(2329); /* daytime server */ bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); listen(listenfd, LISTENQ); for ( ; ; ) { connfd = accept(listenfd, (SA *) NULL, NULL); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); write(connfd, buff, strlen(buff)); close(connfd); } } |
int PASCAL FAR bind (SOCKET s, const struct sockaddr FAR *addr, int namelen);
將一本地地址與一套接口捆綁。本函數適用於未連接的數據報或流類套接口,在connect()或listen()調用前使用。當用socket()創建套接口后,它便存在於一個名字空間(地址族)中,但並未賦名。bind()函數通過給一個未命名套接口分配一個本地名字來為套接口建立本地捆綁(主機地址/端口號)。
int listen( int sockfd, int backlog);
創建一個套接口並監聽申請的連接.
#include <sys/socket.h>
sockfd:用於標識一個已捆綁未連接套接口的描述字。被listen函數作用的套接字,sockfd之前由socket函數返回。在被socket函數 返回的套接字fd之時,它是一個主動連接的套接字,也就是此時系統假設用戶會對這個套接字調用connect函數,期待它主動與其它進程連接,然后在服務 器編程中,用戶希望這個套接字可以接受外來的連接請求,也就是被動等待用戶來連接。由於系統默認時認為一個套接字是主動連接的,所以需要通過某種方式來告 訴系統,用戶進程通過系統調用listen來完成這件事。
backlog:等待連接隊列的最大長度,一般這個值會在30以內。
listen函數使用主動連接套接口變為被連接套接口,使得一個進程可以接受其它進程的請求,從而成為一個服務器進程。在TCP服務器編程中listen函數把進程變為一個服務器,並指定相應的套接字變為被動連接。
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
#include <sys/types.h>
#include <sys/socket.h>
accept()系統調用主要用在基於連接的套接字類型,比如SOCK_STREAM和SOCK_SEQPACKET。它提取出所監聽套接字的等待連接隊列中第一個連接請求,創建一個新的套接字,並返回指向該套接字的文件描述符。新建立的套接字不在監聽狀態,原來所監聽的套接字也不受該系統調用的影響。
sockfd, 利用系統調用socket()建立的套接字描述符,通過bind()綁定到一個本地地址(一般為服務器的套接字),並且通過listen()一直在監聽連接;
addr,指向structsockaddr的指針,該結構用通訊層服務器對等套接字的地址(一般為客戶端地址)填寫,返回地址addr的確切格式由套接字的地址類別(比如TCP或UDP)決定;若addr為NULL,沒有有效地址填寫,這種情況下,addrlen也不使用,應該置為NULL;
addrlen, 一個值結果參數,調用函數必須初始化為包含addr所指向結構大小的數值,函數返回時包含對等地址(一般為服務器地址)的實際數值;
如果隊列中沒有等待的連接,套接字也沒有被標記為Non-blocking,accept()會阻塞調用函數直到連接出現;如果套接字被標記為Non-blocking,隊列中也沒有等待的連接,accept()返回錯誤EAGAIN或EWOULDBLOCK。
int snprintf(char*str, size_t size,constchar*format, ...);
最多從源串中拷貝size-1個字符到目標串中,然后再在后面加一個0。所以如果目標串的大小為size的話,將不會溢出。
若成功則返回欲寫入的字符串長度,若出錯則返回負值。
ssize_t write (int fd, const void * buf, size_t count);
write()會把參數buf 所指的內存寫入count 個字節到參數fd 所指的文件內. 當然, 文件讀寫位置也會隨之移動。
\r\n和\n區別
計算機還沒有出現之前,有一種叫做電傳打字機(Teletype Model 33)的玩意,每秒鍾可以打10個字符。但是它有一個問題,就是打完一行換行的時候,要用去0.2秒,正好可以打兩個字符。要是在這0.2秒里面,又有新的字符傳過來,那么這個字符將丟失。
於是,研制人員想了個辦法解決這個問題,就是在每行后面加兩個表示結束的字符。一個叫做“回車”,告訴打字機把打印頭定位在左邊界;另一個叫做“換行”,告訴打字機把紙向下移一行。
這就是“換行”和“回車”的來歷,從它們的英語名字上也可以看出一二。
后來,計算機發明了,這兩個概念也就被般到了計算機上。那時,存儲器很貴,一些科學家認為在每行結尾加兩個字符太浪費了,加一個就可以。於是,就出現了分歧。Unix 系統里,每行結尾只有“<換行>”,即“\n”;Windows系統里面,每行結尾是“<回車><換行>”,即“ \r\n”;Mac系統里,每行結尾是“<回車>”。一個直接后果是,Unix/Mac系統下的文件在Windows里打開的話,所有文字會變成一行;而Windows里的文件在Unix/Mac下打開的話,在每行的結尾可能會多出一個^M符號。
OSI模型
OSI模型是一個七層模型,從上到下依次為應用層,表示層,會話層,傳輸層,網絡層,數據鏈路層,物理層。我們一般認為下面兩層是隨系統提供的設備驅動程序和網絡硬件,而上面三層一般也合並為一層應用層,我們處理的是傳輸層和網絡層。
傳輸層可選擇TCP或UDP,有時候也會直接繞過傳輸層,直接操作IPv4和IPv6,這稱為原始套接口。網絡層就是IPv4和IPv6。