socket編程在TCP/IP協議中“IP地址+端口號”就稱爲socket。首先我們來看看socket API,即TCP/IP協議的應用層編程接口。
我們接下來編寫一個簡單的客戶端/服務器端通信的簡單的模型,要遵循以下的方法:
1.創建套接字,我們要用到的函數是socket,以下是它的基本信息
其中,domain參數表明我們要用的協議類型,我們用AF_INET(IPV4),type參數表明協議實現的方式,因爲我們都知道,TCP是面向字節流的,因此type就是SOCK_STREAM;protocol我們默認爲0就可以了;
2.填充socket信息,在這裏我們用到一個結構體,如下:
我們要用上面的結構體把IP地址及端口號填充進去,我們後面就可以很方便的使用它了。接下來我們看看參數:
sin_port是端口號,在結構體裏面還有個成員是sin_family,表明我們所使用的協議,我們這裏使用ipv4,所以是AF_INET;sin_addr是ip地址,它也是個結構體,如下:
由於我們通常使用的是點分十進制字符串表示IP地址,以下函數可以在字符串表示和in_addr之間轉換:
我們下面編程使用的是inet_addr這個函數,它的參數cp就是IP地址號,把IP地址轉換爲32位的無符號整數,相反,我們可以使用inet_ntoa這個函數把無符號整數轉換成我們的ip地址。
在計算機內存中有大小端之分,網絡數據流同樣有大小端之分,發送主機通常將發送緩衝區中的數據按內存地址從低到高的順序發出,接收主機也是把從網絡上接收到的字節流從低到高的順序保存,因此,網絡數據流的地址這樣規定:先發出的數據是低地址,後發出的數據是高地址。TCP/IP協議也規定網絡字節序採用大端字節序,即高位數存在低地址。所以會出現這樣一種情況,假如發送端和接收端的字節序不一樣(即一個大端一個小端,這樣是不是就會出問題呢?),所以我們需要考慮字節序的轉換,爲使網絡程序具有可移植性,使同樣的代碼在大端機和小端機上都能運行,我們可以調用下面的函數做網絡字節序和主機字節序的轉換:
在上面的函數中,h代表我們主機,n代表網絡,l代表32位整數,s代表16位短整數,我們在下面的程序中就要用到,比如用htonl函數把端口號進行相應的轉換等。
3.綁定套接字:
我們在上面創建了了一個套接字,並把相應的網絡信息填充到網絡結構體中去了,我們接下來就要把他們進行綁定,用到的函數就是bind,如下:
bind函數爲套接字sockfd指定本地地址my_addr,my_addr的長度爲addrlen(字節),也即給一個套接字分配一個名字。
4.把上面創建的套接字設置爲監聽套接字
用到的函數是listen函數,定義如下:
我們使用上面創建的套接字,調用listen函數使其能夠自動接收到來的連接並且爲連接隊列制定一個長度限制,之後就可以使用accept函數接收連接。其中參數backlog指定未完成連接隊列的最大長度。
5.服務端接收函數accept,定義如下:
accept函數用於基於連接的套接字,它從未完成隊列中取出第一連接請求,創建一個和參數S屬性相同的連接套接字,併爲這個套接字分配一個文件描述符,然後以這個文件描述符返回,新創建的文件描述符不再處於監聽狀態;
6.連接函數(一般用於發送端)connect,定義如下:
下面我們來看看具體的代碼:
服務器端:
#include <iostream> #include <stdlib.h> #include <errno.h> #include <string> #include <string.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <pthread.h> using namespace std; const int g_backlog=5; void usage(string _port) //返回參數錯誤的信息 { cout<<"usage: "<<_port<<"[ip][port]"<<endl; } static int start(const string &ip,const int &_port) { int listen_sock=socket(AF_INET,SOCK_STREAM,0);//創建套接字,參數:ipv4協議,字節流套字 if(listen_sock<0) { cerr<<strerror(errno)<<endl; exit(1); } struct sockaddr_in local; //填充sock信息 local.sin_family=AF_INET; local.sin_port=htons(_port); local.sin_addr.s_addr=inet_addr(ip.c_str()); if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0) //把sock和填充的信息進行綁定 { cerr<<strerror(errno)<<endl; exit(2); } if(listen(listen_sock,g_backlog)<0) //把該套接字設置爲監聽套接字 { cerr<<strerror(errno)<<endl; exit(3); } return listen_sock; } void *thread_run(void *arg) { int sock =(int)arg; char buf[1024]; while(1) { memset(buf,'\0',sizeof(buf)); ssize_t _size=read(sock,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { cout<<"client close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<"client# "<<buf<<endl; } close(sock); return NULL; } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } struct sockaddr_in client; socklen_t len=sizeof(client); string ip=argv[1]; int port=atoi(argv[2]); int listen_sock=start(ip,port); while(1) { int new_fd=accept(listen_sock,(struct sockaddr*)&client,&len); if(new_fd<0) { cerr<<strerror(errno)<<endl; continue; } cout<<"get a connect..."<<" sock :"<<new_fd<<" ip:"<<inet_ntoa(client.sin_addr)<<" port:"<<ntohs(client.sin_port)<<endl; #ifdef _V1_ char buf[1024]; while(1) { string _client=inet_ntoa(client.sin_addr); ssize_t _size=read(new_fd,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { //client close cout<<_client<<"close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<"client#:"<<buf<<endl; } #elif _V2_ cout <<"v2"<<endl; pid_t id=fork(); if(id==0) { string _client=inet_ntoa(client.sin_addr); close(listen_sock); char buf[1024]; while(1) { memset(buf,'\0',sizeof(buf)); ssize_t _size=read(new_fd,buf,sizeof(buf)-1); if(_size>0) { buf[_size]='\0'; } else if(_size==0) { cout<<_client<<"close..."<<endl; break; } else { cout<<strerror(errno)<<endl; } cout<<_client<<"# "<<buf<<endl; } close(new_fd); exit(0); } else if(id>0) { close(new_fd); } else {} #elif _V3_ pthread_t tid; pthread_create(&tid,NULL,thread_run,(void*)new_fd); pthread_detach(tid); #else cout<<"default"<<endl; #endif } return 0; } 客戶端程序: void usage(string _port) //同樣檢查參數輸入錯誤 { cout<<_port<<"[remote ip][remote port]"<<endl; } int main(int argc,char* argv[]) { if(argc!=3) { usage(argv[0]); exit(1); } string ip=argv[1]; int r_port=atoi(argv[2]); int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<-1) { cout<<strerror(errno)<<endl; exit(1); } struct sockaddr_in remote; remote.sin_family=AF_INET; remote.sin_port=htons(r_port); remote.sin_addr.s_addr=inet_addr(ip.c_str()); int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote)); if(ret<0) { cout<<strerror(errno)<<endl; } string msg; while(1) { cout<<"plenter enter: "; cin >> msg; write(sock,msg.c_str(),msg.size()); } return 0; }
從上面的代碼中我們可以看到,我們有3個版本,我們下面會介紹每一版本的不同,我們先用其中的一個版本看一下結果(V1版):
從上圖我們可以看到,客戶端發給服務器段的數據被服務器端收到了,證明連接建立成功了,可以收發數據了。
接下來我們說說上面幾個版本的差別:
版本一(_V1_):版本一我們可以發現,版本一的服務端只能處理一個請求,服務端在收到一個請求時,就去處理這個請求,不再處於監聽狀態了,因此當再有請求來時,就不會處理這個請求了,顯然這是個單處理模式,日常生活中這是不符合實際的,因此我們就衍生出了版本二(_V2_);
版本二(_V2_):在版本二中我們可以看到,我們用fork()一個子進程,讓子進程處理客戶端的連接請求,而父進程繼續處於監聽狀態,等待下一個請求,這樣就可以處理多個連接請求了,我們運行版本二看下結果:
我們可以看到,有兩個客戶端連接上了併發送了一些數據(由於我們是在同一臺機子上測試,所以ip地址相同,端口號不同),這樣我們就實現了多請求處理。在上面的版本二中,我們可以看見父進程並沒有等在子進程,這樣子進程運行結束後就會變成殭屍進程,但如果父進程以阻塞方式等待,就會和版本一一樣(一次只能處理一個請求),如果以非阻塞方式等待,子進程同樣不會被回收。我們也可以這樣處理:我們註冊一個信號處理函數,因爲我們都知道子進程退出後,會給父進程發送一個SIGCHLD信號,我們可用捕捉該信號的方法來處理這種情況,同樣,我們也可以利用線程來處理,就是版本三(_V3_);
版本三(_V3_):在版本三中,我們創建一個線程,並把線程這之城分離的,這樣線程在退出的時候它的資源就會被主動回收;
現在 我們再來看一種情況,就是先啓動server端,然後客戶端連接上,然後用Ctrl+c,使server端退出,再次運行server端,就會出現如下情況:
這時因爲孫然server端應用程序終止了,但TCP協議層的連接並沒有完全斷開,因此不能再次監聽同樣的server端口,那麼我們如何如何解決上述問題,使server端口在2MSL時間內能再次進行監聽來接受新的連接呢?我們可以使用setsockopt函數,定義如下:
參數level爲SOL_SOCKET,optname設置爲SO_REUSEADDR;
我們只需在server端的代碼socket函數和bind函數之間加上如下代碼就可以了:
添加完上述代碼後,我們再次運行程序,結果如下:
我們可以看到,當我們運行server端程序,收到一個連接後,我們馬航退出server,再次運行server端不會再出現“地址被佔用”的提示了,好了,上述問題搞定!!
至此,完成一個簡單的客戶/服務器基於TCP的服務。