一、 套接字:源IP地址和目的IP地址以及源端口號和目的端口號的組合稱爲套接字。其用於標識客戶端請求的服務器和服務。編程
經常使用的TCP/IP協議的3種套接字類型以下所示。
(1)流套接字(SOCK_STREAM):數組流套接字用於提供面向鏈接、可靠的數據傳輸服務。該服務將保證數據可以實現無差錯、無重複發送,並按順序接收。流套接字之因此可以實現可靠的數據服務,緣由在於其使用了傳輸控制協議,即TCP(The Transmission ControlProtocol)協議。(2)數據報套接字(SOCK_DGRAM):緩存
數據報套接字提供了一種無鏈接的服務。該服務並不能保證數據傳輸的可靠性,數據有可能在傳輸過程當中丟失或出現數據重複,且沒法保證順序地接收到數據。數據報套接字使用UDP(User Datagram Protocol)協議進行數據的傳輸。因爲數據報套接字不能保證數據傳輸的可靠性,對於有可能出現的數據丟失狀況,須要在程序中作相應的處理。(3) 原始套接字(SOCK_RAW):(通常不用這個套接字)服務器
原始套接字(SOCKET_RAW)容許對較低層次的協議直接訪問,好比IP、 ICMP協議,它經常使用於檢驗新的協議實現,或者訪問現有服務中配置的新設備,由於RAW SOCKET能夠自如地控制Windows下的多種協議,可以對網絡底層的傳輸機制進行控制,因此能夠應用原始套接字來操縱網絡層和傳輸層應用。好比,咱們能夠經過RAW SOCKET來接收發向本機的ICMP、IGMP協議包,或者接收TCP/IP棧不可以處理的IP包,也能夠用來發送一些自定包頭或自定協議的IP包。網絡監聽技術很大程度上依賴於SOCKET_RAW二、 套接字基本函數:網絡
(1) 建立套接字:
int socket(int family, int type, intprotocol);
功能介紹:
在Linux操做系統中,一切皆文件,這個你們都知道,我的理解建立socket的過程其實就是一個得到文件描述符的過程,固然這個過程會是比較複雜的。能夠從內核中找到建立socket的代碼,而且socket的建立和其餘的listen,bind等操做分離開來。socket函數完成正確的操做是返回值大於0的文件描述符,當返回小於0的值時,操做錯誤。一樣是返回一個文件描述符,可是會由於三個參數組合不一樣,對於數據具體的工做流程不一樣,對於應用層編程來講,這些也是不可見的。
參數說明:
從socket建立的函數能夠看出,socket有三個參數,family表明一個協議族,比較熟知的就是AF_INET,PF_PACKET等;第二個參數是協議類型,常見類型是SOCK_STREAM,SOCK_DGRAM, SOCK_RAW, SOCK_PACKET等;第三個參數是具體的協議,對於標準套接字來講,其值是0,對於原始套接字來講就是具體的協議值。
(2) 套接字綁定函數:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
功能介紹:
bind函數主要應用於服務器模式一端,其主要的功能是將addrlen長度 structsockaddr類型的myaddr地址與sockfd文件描述符綁定到一塊兒,在sockaddr中主要包含服務器端的協議族類型,網絡地址和端口號等。在客戶端模式中不須要使用bind函數。當bind函數返回0時,爲正確綁定,返回-1,則爲綁定失敗。
參數說明:
bind函數的第一個參數sockfd是在建立socket套接字時返回的文件描述符。
bind函數的第二個參數是structsockaddr類型的數據結構,因爲structsockaddr數據結構類型不方便設置,因此一般會經過對tructsockaddr_in進行地質結構設置,而後進行強制類型轉換成structsockaddr類型的數據,
(3) 監聽函數:
int listen(int sockfd, int backlog);
功能介紹:數據結構
剛開始理解listen函數會有一個誤區,就是認爲其操做是在等在一個新的connect的到來,其實不是這樣的,真正等待connect的是accept操做,listen的操做就是當有較多的client發起connect時,server端不能及時的處理已經創建的鏈接,這時就會將connect鏈接放在等待隊列中緩存起來。這個等待隊列的長度有listen中的backlog參數來設定。listen和accept函數是服務器模式特有的函數,客戶端不須要這個函數。當listen運行成功時,返回0;運行失敗時,返回值位-1.多線程
參數說明:併發
sockfd是前面socket建立的文件描述符;backlog是指server端能夠緩存鏈接的最大個數,也就是等待隊列的長度。
(4) 請求接收函數:int accept(int sockfd, structsockaddr *client_addr, socklen_t *len);
功能介紹:
接受函數accept其實並非真正的接受,而是客戶端向服務器端監聽端口發起的鏈接。對於TCP來講,accept從阻塞狀態返回的時候,已經完成了三次握手的操做。Accept實際上是取了一個已經處於connected狀態的鏈接,而後把對方的協議族,網絡地址以及端口都存在了client_addr中,返回一個用於操做的新的文件描述符,該文件描述符表示客戶端與服務器端的鏈接,經過對該文件描述符操做,能夠向client端發送和接收數據。同時以前socket建立的sockfd,則繼續監聽有沒有新的鏈接到達本地端口。返回大於0的文件描述符則表示accept成功,不然失敗。
參數說明:
sockfd是socket建立的文件描述符;client_addr是本地服務器端的一個structsockaddr類型的變量,用於存放新鏈接的協議族,網絡地址以及端口號等;第三個參數len是第二個參數所指內容的長度,對於TCP來講其值能夠用sizeof(structsockaddr_in)來計算大小,說要說明的是accept的第三個參數要是指針的形式,由於這個值是要傳給協議棧使用的。
(5)客戶端請求鏈接函數:int connect(int sock_fd, struct sockaddr *serv_addr,int addrlen);
功能介紹:socket鏈接函數connect是屬於client端的操做函數,其目的是向服務器端發送鏈接請求,這也是從客戶端發起TCP三次握手請求的開始,服務器端的協議族,網絡地址以及端口都會填充到connect函數的serv_addr地址當中。當connect返回0時說明已經connect成功,返回值是-1時,表示connect失敗。
參數說明:函數connect的第一個參數是socket建立的文件描述符;第二個參數是一個structsockaddr類型的指針,這個參數中設置的是要鏈接的目標服務器的協議族,網絡地址以及端口號;第三個參數表示第二個參數內容的大小,與accept不一樣,這個值不是一個指針。
在服務器端和客戶端創建鏈接以後是進行數據間的發送和接收,主要使用的接收函數是recv和read,發送函數是send和write。由於對於socket套接字來講,最終實際操做的是文件描述符,因此可使用對文件進行操做的接收和發送函數對socket套接字進行操做。read和write函數是文件編程裏的知識,因此這裏再也不作多與的贅述。
三、 有了以上的知識,那麼咱們就能夠編寫一個簡單的服務器和客戶端了
(1) 簡易服務器:這個服務器只能與一個客戶端相鏈接,若是有多個客戶端就不能用這個服務器進行鏈接。代碼:
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #define PORT 9990 //端口號 #define SIZE 1024 //定義的數組大小 int Creat_socket() //建立套接字和初始化以及監聽函數 { int listen_socket = socket(AF_INET, SOCK_STREAM, 0); //建立一個負責監聽的套接字 if(listen_socket == -1) { perror("socket"); return -1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; /* Internet地址族 */ addr.sin_port = htons(PORT); /* 端口號 */ addr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址 */ int ret = bind(listen_socket, (struct sockaddr *)&addr, sizeof(addr)); //鏈接 if(ret == -1) { perror("bind"); return -1; } ret = listen(listen_socket, 5); //監聽 if(ret == -1) { perror("listen"); return -1; } return listen_socket; } int wait_client(int listen_socket) { struct sockaddr_in cliaddr; int addrlen = sizeof(cliaddr); printf("等待客戶端鏈接。。。。\n"); int client_socket = accept(listen_socket, (struct sockaddr *)&cliaddr, &addrlen); //建立一個和客戶端交流的套接字 if(client_socket == -1) { perror("accept"); return -1; } printf("成功接收到一個客戶端:%s\n", inet_ntoa(cliaddr.sin_addr)); return client_socket; } void hanld_client(int listen_socket, int client_socket) //信息處理函數,功能是將客戶端傳過來的小寫字母轉化爲大寫字母 { char buf[SIZE]; while(1) { int ret = read(client_socket, buf, SIZE-1); if(ret == -1) { perror("read"); break; } if(ret == 0) { break; } buf[ret] = '\0'; int i; for(i = 0; i < ret; i++) { buf[i] = buf[i] + 'A' - 'a'; } printf("%s\n", buf); write(client_socket, buf, ret); if(strncmp(buf, "end", 3) == 0) { break; } } close(client_socket); } int main() { int listen_socket = Creat_socket(); int client_socket = wait_client(listen_socket); hanld_client(listen_socket, client_socket); close(listen_socket); return 0; }
(2) 多進程併發服務器:該服務器就徹底彌補了上一個服務器的不足,能夠同時處理多個客戶端,只要有客戶端來鏈接它,他就能響應。在咱們這個服務器中,父進程主要負責監聽,因此在父進程一開始就要把父進程的接收函數關閉掉,防止父進程在接收函數處阻塞,致使子進程不能建立成功。同理,子進程主要負責接收客戶端,並作相關處理,因此子進程在一建立就要把監聽函數關閉,否則會致使服務器功能的紊亂。這個服務器有一個特別要注意的是,子進程在退出時會產生殭屍進程,因此咱們必定要對子進程退出後進行處理。
代碼:
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #define PORT 9990 #define SIZE 1024 int Creat_socket() //建立套接字和初始化以及監聽函數 { int listen_socket = socket(AF_INET, SOCK_STREAM, 0); //建立一個負責監聽的套接字 if(listen_socket == -1) { perror("socket"); return -1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; /* Internet地址族 */ addr.sin_port = htons(PORT); /* 端口號 */ addr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址 */ int ret = bind(listen_socket, (struct sockaddr *)&addr, sizeof(addr)); //鏈接 if(ret == -1) { perror("bind"); return -1; } ret = listen(listen_socket, 5); //監聽 if(ret == -1) { perror("listen"); return -1; } return listen_socket; } int wait_client(int listen_socket) { struct sockaddr_in cliaddr; int addrlen = sizeof(cliaddr); printf("等待客戶端鏈接。。。。\n"); int client_socket = accept(listen_socket, (struct sockaddr *)&cliaddr, &addrlen); //建立一個和客戶端交流的套接字 if(client_socket == -1) { perror("accept"); return -1; } printf("成功接收到一個客戶端:%s\n", inet_ntoa(cliaddr.sin_addr)); return client_socket; } void hanld_client(int listen_socket, int client_socket) //信息處理函數,功能是將客戶端傳過來的小寫字母轉化爲大寫字母 { char buf[SIZE]; while(1) { int ret = read(client_socket, buf, SIZE-1); if(ret == -1) { perror("read"); break; } if(ret == 0) { break; } buf[ret] = '\0'; int i; for(i = 0; i < ret; i++) { buf[i] = buf[i] + 'A' - 'a'; } printf("%s\n", buf); write(client_socket, buf, ret); if(strncmp(buf, "end", 3) == 0) { break; } } close(client_socket); } void handler(int sig) { while (waitpid(-1, NULL, WNOHANG) > 0) { printf ("成功處理一個子進程的退出\n"); } } int main() { int listen_socket = Creat_socket(); signal(SIGCHLD, handler); //處理子進程,防止殭屍進程的產生 while(1) { int client_socket = wait_client(listen_socket); //多進程服務器,能夠建立子進程來處理,父進程負責監聽。 int pid = fork(); if(pid == -1) { perror("fork"); break; } if(pid > 0) { close(client_socket); continue; } if(pid == 0) { close(listen_socket); hanld_client(listen_socket, client_socket); break; } } close(listen_socket); return 0; }
(3) 多線程併發服務器:上一個多進程服務器有一個缺點,就是每當一個子進程獲得響應的時候,都要複製父進程的一切信息,這樣就致使了CPU資源的浪費,當客戶端有不少來鏈接這個服務器的時候,就會產生不少的子進程,會致使服務器的響應變得很慢。因此咱們就想到了多線程併發服務器,咱們知道線程的速度是進程的30倍左右,因此咱們就用線程來作服務器。
代碼:
#include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #define PORT 9990 #define SIZE 1024 int Creat_socket() //建立套接字和初始化以及監聽函數 { int listen_socket = socket(AF_INET, SOCK_STREAM, 0); //建立一個負責監聽的套接字 if(listen_socket == -1) { perror("socket"); return -1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; /* Internet地址族 */ addr.sin_port = htons(PORT); /* 端口號 */ addr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址 */ int ret = bind(listen_socket, (struct sockaddr *)&addr, sizeof(addr)); //鏈接 if(ret == -1) { perror("bind"); return -1; } ret = listen(listen_socket, 5); //監聽 if(ret == -1) { perror("listen"); return -1; } return listen_socket; } int wait_client(int listen_socket) { struct sockaddr_in cliaddr; int addrlen = sizeof(cliaddr); printf("等待客戶端鏈接。。。。\n"); int client_socket = accept(listen_socket, (struct sockaddr *)&cliaddr, &addrlen); //建立一個和客戶端交流的套接字 if(client_socket == -1) { perror("accept"); return -1; } printf("成功接收到一個客戶端:%s\n", inet_ntoa(cliaddr.sin_addr)); return client_socket; } void hanld_client(int listen_socket, int client_socket) //信息處理函數,功能是將客戶端傳過來的小寫字母轉化爲大寫字母 { char buf[SIZE]; while(1) { int ret = read(client_socket, buf, SIZE-1); if(ret == -1) { perror("read"); break; } if(ret == 0) { break; } buf[ret] = '\0'; int i; for(i = 0; i < ret; i++) { buf[i] = buf[i] + 'A' - 'a'; } printf("%s\n", buf); write(client_socket, buf, ret); if(strncmp(buf, "end", 3) == 0) { break; } } close(client_socket); } int main() { int listen_socket = Creat_socket(); while(1) { int client_socket = wait_client(listen_socket); pthread_t id; pthread_create(&id, NULL, hanld_client, (void *)client_socket); //建立一個線程,來處理客戶端。 pthread_detach(id); //把線程分離出去。 } close(listen_socket); return 0; } (4)客戶端:客戶端相對於服務器來講就簡單多了,客戶端只須要建立和服務器相鏈接的套接字,而後對其初始化,而後再進行鏈接就能夠了,鏈接上服務器就能夠發送你想發送的數據了。 代碼: #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #define PORT 9990 #define SIZE 1024 int main() { int client_socket = socket(AF_INET, SOCK_STREAM, 0); //建立和服務器鏈接套接字 if(client_socket == -1) { perror("socket"); return -1; } struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; /* Internet地址族 */ addr.sin_port = htons(PORT); /* 端口號 */ addr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址 */ inet_aton("127.0.0.1", &(addr.sin_addr)); int addrlen = sizeof(addr); int listen_socket = connect(client_socket, (struct sockaddr *)&addr, addrlen); //鏈接服務器 if(listen_socket == -1) { perror("connect"); return -1; } printf("成功鏈接到一個服務器\n"); char buf[SIZE] = {0}; while(1) //向服務器發送數據,並接收服務器轉換後的大寫字母 { printf("請輸入你相輸入的:"); scanf("%s", buf); write(client_socket, buf, strlen(buf)); int ret = read(client_socket, buf, strlen(buf)); printf("buf = %s", buf); printf("\n"); if(strncmp(buf, "END", 3) == 0) //當輸入END時客戶端退出 { break; } } close(listen_socket); return 0; }