TCP/IP、UDP設計模式
TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業標準的協議集,它是爲廣域網(WANs)設計的。 數組
TCP/IP協議存在於OS中,網絡服務經過OS提供,在OS中增長支持TCP/IP的系統調用——Berkeley套接字,如Socket,Connect,Send,Recv等瀏覽器
UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬於TCP/IP協議族中的一種。如圖:服務器
TCP/IP協議族包括運輸層、網絡層、鏈路層,而socket所在位置如圖,Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層。網絡
socket起源於Unix,而Unix/Linux基本哲學之一就是「一切皆文件」,均可以用「打開open –> 讀寫write/read –> 關閉close」模式來操做。Socket就是該模式的一個實現, socket便是一種特殊的文件,一些socket函數就是對其進行的操做(讀/寫IO、打開、關閉).
說白了Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。
數據結構
對於每一個程序系統都有一張單獨的表。精確地講,系統爲每一個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統把一個指向此文件內部數據結構的指針寫入文件描述符表,並把該表的索引值返回給調用者 。應用程序只需記住這個描述符,並在之後操做該文件時使用它。操做系統把該描述符做爲索引訪問進程描述符表,經過指針找到保存該文件全部的信息的數據結構。socket
SOCKET接口函數ide
工做原理:「open—write/read—close」模式。函數
服務器端先初始化Socket,而後與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端鏈接。在這時若是有個客戶端初始化一個Socket,而後鏈接服務器(connect),若是鏈接成功,這時客戶端與服務器端的鏈接就創建了。客戶端發送數據請求,服務器端接收請求並處理請求,而後把迴應數據發送給客戶端,客戶端讀取數據,最後關閉鏈接,一次交互結束。測試
bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。函數的三個參數分別爲:
sockfd:即socket描述字,它是經過socket()函數建立了,惟一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不一樣而不一樣,如ipv4對應的是:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
addrlen:對應的是地址的長度。
一般服務器在啓動的時候都會綁定一個衆所周知的地址(如ip地址+端口號),用於提供服務,客戶就能夠經過它來接連服務器;而客戶端就不用指定,有系統自動分配一個端口號和自身的ip地址組合。這就是爲何一般服務器端在listen以前會調用bind(),而客戶端就不會調用,而是在connect()時由系統隨機生成一個。
注意:
網絡字節序以大端模式傳輸。因此:在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序
做爲一個服務器,在調用socket()、bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數的第一個參數即爲要監聽的socket描述字,第二個參數爲相應socket能夠排隊的最大鏈接個數。socket()函數建立的socket默認是一個主動類型的,listen函數將socket變爲被動類型的,等待客戶的鏈接請求。
connect函數的第一個參數即爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。客戶端經過調用connect函數來創建與TCP服務器的鏈接。
TCP服務器端依次調用socket()、bind()、listen()以後,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()以後就向TCP服務器發送了一個鏈接請求。TCP服務器監聽到這個請求以後,就會調用accept()函數取接收請求,這樣鏈接就創建好了。以後就能夠開始網絡I/O操做了,即類同於普通文件的讀寫I/O操做。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
參數sockfd
參數sockfd就是上面解釋中的監聽套接字,這個套接字用來監聽一個端口,當有一個客戶與服務器鏈接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。固然客戶不知道套接字這些細節,它只知道一個地址和一個端口號。
參數addr
這是一個輸出型參數,它用來接受一個返回值,這返回值指定客戶端的地址,固然這個地址是經過某個地址結構來描述的,用戶應該知道這一個什麼樣的地址結構。若是對客戶的地址不感興趣,那麼能夠把這個值設置爲NULL。
參數len
如同你們所認爲的,它也是輸出型參數,用來接受上述addr的結構的大小的,它指明addr結構所佔有的字節個數。一樣的,它也能夠被設置爲NULL。
若是accept成功返回,則服務器與客戶已經正確創建鏈接了,此時服務器經過accept返回的套接字來完成與客戶的通訊。
注意:
accept默認會阻塞進程,直到有一個客戶鏈接創建後返回,它返回的是一個新可用的套接字,這個套接字是鏈接套接字。
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上能夠把上面的其它函數都替換成這兩個函數。它們的聲明以下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
這幾個函數比較簡單,就不做詳細介紹了。
在服務器與客戶端創建鏈接以後,會進行一些讀寫操做,完成了讀寫操做就要關閉相應的socket描述字,比如操做完打開的文件要調用fclose關閉打開的文件。
#include <unistd.h>
int close(int fd);
close一個TCP socket的缺省行爲時把該socket標記爲以關閉,而後當即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再做爲read或write的第一個參數。
注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。
示例:
TCP通訊
服務器
//sever.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #include <string.h> void* handler_data(void* arg) { int sock = *((int*)arg); printf("connect a new client %d\n", sock); char buf[1024]; memset(buf, '\0', sizeof(buf)); while(1) { ssize_t _s = read(sock, buf, sizeof(buf)-1); if(_s > 0) { buf[_s] = '\0'; printf("client[%d] # %s\n", sock, buf); write(sock, buf, strlen(buf)); } else if(_s == 0) { printf("client[%d] is closed...\n", sock); break; } else { break; } } close(sock); pthread_exit(NULL); } int main() { int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if(listen_sock < 0) { perror("socket"); return 1; } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_port = htons(8080); local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listen_sock, (const struct sockaddr*)&local, sizeof(local)) < 0) { perror("bind"); return 2; } if(listen(listen_sock, 5) < 0) { perror("listen"); return 3; } struct sockaddr_in peer; socklen_t len = sizeof(peer); while(1) { int new_fd = accept(listen_sock, (struct sockaddr*)&peer, &len); if(new_fd > 0) { pthread_t id; pthread_create(&id, NULL, handler_data, (void* )&new_fd); pthread_detach(id); } } return 0; }
客戶端
//client.c #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <pthread.h> #include <string.h> int main(int argc, char* argv[]) { if(argc != 3) { printf("error argv\n"); return 1; } int conn_sock = socket(AF_INET, SOCK_STREAM, 0); if(conn_sock < 0) { perror("socket"); return 2; } struct sockaddr_in remote; remote.sin_family = AF_INET; remote.sin_port = htons(atoi(argv[2])); remote.sin_addr.s_addr = inet_addr(argv[1]); if(connect(conn_sock, (const struct sockaddr*)&remote, sizeof(remote)) < 0) { perror("connect"); return 3; } char buf[1024]; memset(buf, '\0', sizeof(buf)); while(1) { printf("please enter# "); fflush(stdout); ssize_t _s = read(0, buf, sizeof(buf)-1); if(_s > 0) { buf[_s-1] = '\0'; write(conn_sock, buf, strlen(buf)); read(conn_sock, buf, sizeof(buf)); printf("sever echo# %s\n", buf); } } return 0; }
程序演示:
運行服務器後,服務器等待TCP鏈接,這裏能夠用三種方式測試:Telnet、瀏覽器、客戶端。
Telnet測試:
瀏覽器測試:
客戶端測試:
注意:在啓動服務器的時候可能會出現以下的狀況:
如今用Ctrl-C把client終止掉,等待大約30秒後,服務器又能夠啓動了。
緣由分析:
雖然server的應用程序終止了,但TCP協議層的鏈接並無徹底斷開,所以不能再次監 聽一樣的server端口。
client終止時自動關閉socket描述符,server的TCP鏈接收到client發送的FIN段後處於TIME_WAIT狀態。TCP協議規定,主動關閉鏈接的一方要處於TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間後才能回到CLOSED狀態,由於咱們先Ctrl-C終止了server,因此server是主動關閉鏈接的一方,在TIME_WAIT期間仍然不能再次監聽一樣的server端口。MSL在RFC1122中規定爲兩分鐘,可是各操做系統的實現不一樣,在Linux上通常通過半分鐘後就能夠再次啓動server了。
解決這個問題的方法是使用setsockopt()設置socket描述符的選項SO_REUSEADDR爲1,表示容許建立端口號相同但IP地址不一樣的多個socket描述符。在server代碼的socket()和bind()調用之間插入以下代碼:
setsocketopt這個函數這裏不做詳細介紹,有興趣的讀者能夠自行查詢一下。