如你所知,簡單的網絡編程就是稱為客戶端和服務器的兩台主機進行通信。顯然通信雙方要有一個統一的標識,電話機的比方就很好。這個標識不僅僅是IP地址或者端口號,我們可以將二者結合起來。稱之為套接字,socket。在網絡編程中socket無疑是關鍵的部分,因此網絡編程也常常被叫做socket編程。
叫什么不重要,重要的是原理。本文的目的也正是這樣,我們試圖搞懂它。
為了減少篇幅,關於協議,TCP,UDP,ISO七層模型等,這些基礎知識這里就不做說明了。我們采用的是客戶/服務器的模式。又客戶對服務器做出通信請求,而服務器對其響應。另外本文的所有程序都可以在vc6.0中實現。
首先我們先大體文字介紹一下雙方的通信流程。比如服務器端:
1,打開通信通道並告知本地主機,服務器端可在某一地址和端口接收客戶端請求。
2,等待客戶請求到達該端口。
3,接收到重復的服務請求,處理並發送應答信號。如果是並發的請求,那么就啟動一個新進程。服務完成后,關閉此進程與客戶的通信鏈路,並終止。
4,返回第二步,也就是繼續等待新的請求
5,關閉服務器
客戶端所做的要更為簡單
1,打開一個通信通道,並連接到服務器所在主機的特定端口
2,向服務器端發送請求報文,等待並接受應答,然后可繼續提出請求
3,通信結束,關閉通信通道並終止
為了更接近程序實現,可以吧流程簡化一下。當然TCP和UDP的實現方法略有不同。事實上windows socket程序只是在伯克利的基礎上加了一下異步函數,和符合wondows消息驅動特性的網絡事件異步選擇機制。根據TCP和UDP的協議遠離的區別,可以分為流式套接字(SOCK_STREAM)和數據包套接字(SOCK_DGRAM)
流式套接字服務器端可分為七步
1,創建套接字(socket函數)
2,將套接字綁定到指定端口上(bind函數)
3,將套接字設定為監聽模式,准備接受客戶請求(listen函數)
4,等待客戶請求的到來,當請求到來后接收連接請求,返回一個新的對應於此連接的套接字(accept函數)
5,用返回的套接字和客戶進行通信(send/recv函數)
6,返回,等待一個新的客戶請求
7,關閉套接字(closesocket函數)
客戶端程序可分為四步
1,創建套接字
2,向服務器發出鏈接請求(connect函數)
3, 和服務器進行通信(send/recv函數)
4, 關閉套接字
基於UDP的套接字服務器端稍有不同,創建,綁定,接收,關閉即可因為是無連接的少去了監聽和等待的步驟。同樣在客戶端也少去了請求連接的過程。
TCP代碼實現:首先要建兩個工程,不妨設為tcpsrv和tcpclient,分別為客戶端和服務器端
tcpsrv.cpp
/*第一步:加載套接字庫 WSAstartup函數,另外也可對套接字庫進行版本協商,兩個參數,wVersionRequested指定請求的版本號,需要注意的是高字節指定副版本,低字節指定主版本。第二個參數是一個指向wsadata的指針,WSAStartup用其加載的庫版本有關的信息填在這個結構中,接收windows socket */ WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( );//終止對winsock庫的使用 return; } /* 1.創建套接字,socket函數,返回一個套接字的描述符,三個參數,一個參數af指定地址族,第二個參數指定套接字的類型也就是TCP或UDP,第三個參數是與特定的地址家族相關的協議,如果指定為0,那么他就根據地址格式和套接字的類別, 自動為你設置一個合適的協議 如果該函數調用成功,他將返回一個新的SOCKET數據類型的套接字描述符。如果失敗則返回一個 INVALID_SOCKET錯誤信息通過WSAGetLastID_SOCKET函數返回。 */ SOCKET socksrv=socket(AF_INET,SOCK_STREAM,0); /*2.綁定套接字函數bind,接收三個參數,第一個指定要綁定的套接字,第二個參數指定該套接字的本地地址信息,是一個指向sockaddr 結構的指針變量,由於地址結構是為所有地址家族准備的,這個結構可能隨所擁有網絡協議不同而不同。所以要用第三個參數指定該地址結構的長度,顯然要事先定義sockaddr結構體。 另外,因為實際要求的是內存區,所以對於不同的協議家族,用不同的結構來替換。常用的兩個函數 inet_addr(),將點十進制的IP地址轉換為適合分配給s_addr的u_long類型的數值 inet_ntoa()函數起相反的作用*/ SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY) addrsrv.sin_family=AF_INET; addrsrv.sin_port=htons(6000); bind(socksrv,(SOCKADDR*)&addrsrv,sizeof(SOCKADDR)); //4.將套接字設置為監聽模式listen函數,第一個參數指定套機字,第二個參數設置等待連接隊列的長度,此處設為5 listen(socaksrv,5); /*5.設置循環,不斷地連接請求的到來, 首先定義一個地址結構體變量來接受客戶的地質結構信息。然后定一個整形變量存儲地址長度,為防止調用失敗設置一個初始值 */ SOCKADDR_IN addrClient; int len=sizeof(SOCKADDR); /* 6.在while循環中,用一個函數來等待客戶的連接到來並接受客戶的連接請求。accept函數,成功后會返回一個新的連接套接字的描述符,而原來的套接字則繼續監聽客戶的連接請求 */ while(1) { SOCKET sockConn=accept(socksrv,(SOCKADDR*)&addrClient,&len); /*7.進行通信,向客戶端發送數據,send函數 */ char sendBuf[100]; sprintf(sendBuf,"welcome %s to MFC",inet_ntoa(addrClient.sin_addr));//ip地址 send(sockConn,sendBuf,strlen(sendBuf)+1,0); /*8.同時可以從客戶端接受數據用recv函數*/ char recvBuf[100]; recv(sockConn,recvBuf,100,0); printf("%s\n",recvBuf);//打印接收到的數據 //9.關閉套接字 closesocket(sockConn); }
相應的客戶端程序,tcpclient.cpp
void main() { WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //第一步創建套接字 SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0); //第二步,因為客戶端不需要綁定,可直接去連接服務器端, //connect函數 SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1") //127.0.0.1因為只在本機測試,故此處用本機的IP地址 addrsrv.addr_family=AF_INET; addrsrv.addr_port=htons(6000); connect(sockClient,(SOCKADDR*)&addrsrv,sizeof(SOCKADDR)); //第三步接收數據 char recvBuf[100]; recv(sockClient,recvBuf,100,0); printf("%s\n",recvBuf);//將接受到的數據打印出來 send(sockClient,"this is client",strlen("this is client")+1,0); //第四步 關閉套接字 closesocket(sockClient); WSAClenup(); }
UDP的相對來說比較簡單
客戶端:
void main() { WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //第一步同樣是新建套接字 SOCKET sockClient=socket(AF_INET,SOCK_DGRAM,0); //第二步直接發送數據 用sendto函數 /* int sendto( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, 設定目的套接字的地址結構體信息 int tolen 地址結構體長度 ); */ SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); addrsrv.sin_family=AF_INET; addrsrv.sin_port=htons(6000); sendto(sockClient,"hello",strlen("hello")+1,0, (SOCKADDR*)&addrsrv,sizeof(SOCKADDR)); closesocket(sockClient); WSACleanup(); }
服務器端:
void main() { WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } //第一步同樣的創建套接字 SOCKET socksrv=socket(AF_INET,SOCK_DGRAM,0); SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrsrv.sin_family=AF_INET; addrsrv.sin_port=htons(6000); //綁定套接字 bind(socksrv,(SOCKADDR*)&addrsrv,sizeof(SOCKADDR)); /*接收數據 此處與TCP不同 int recvfrom( SOCKET s, char FAR* buf, 接收數據的buf int len, 長度 int flags, 0 struct sockaddr FAR *from, 地址結構體指針,表明是一個返回izhi int FAR *fromlen 設置初始值,並且有返回值 ); 首先定義地址結構的變量 */ SOCKADDR_IN addrClient; int len=sizeof(SOCKADDR); char recvBuf[100]; recvfrom(socksrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len); printf("%s \n",recvBuf); closesocket(socksrv); WSACleanup(); }
另外加貼一個具有聊天功能的小程序,udp實現
服務器端:
void main() { WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } SOCKET socksrv=socket(AF_INET,SOCK_DGRAM,0); SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrsrv.sin_family=AF_INET; addrsrv.sin_port=htons(6000); bind(socksrv,(SOCKADDR*)&addrsrv,sizeof(SOCKADDR)); char recvBuf[100]; char sendBuf[100]; char tempBuf[200]; SOCKADDR_IN addrClient; int len=sizeof(SOCKADDR); while(1) { recvfrom(socksrv,recvBuf,100,0,(SOCKADDR*)&addrClient,&len); if('q'==recvBuf[0])//判斷如果第一個字符是Q則退出 { sendto(socksrv,"q",strlen("q")+1,0,(SOCKADDR*)&addrClient,len); printf("chat end !\n"); break; } sprintf(tempBuf,"%s say:%s",inet_ntoa(addrClient.sin_addr),recvBuf); //如果不是Q則將客戶端的IP地址和發送的數據格式化完放到tempBUf中 printf("%s\n",tempBuf); printf("please input data:\n"); //等待用戶輸入 gets(sendBuf);//從標准輸入流中獲取一行數據 sendto(socksrv,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrClient,len); //將用戶輸入的數據發送到客戶端 } closesocket(socksrv); WSACleanup(); }
客戶端:
void main() { WORD wVersionRequested;//定義一個word類型的變量 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return; } SOCKET sockClient=socket(AF_INET,SOCK_DGRAM,0); SOCKADDR_IN addrsrv; addrsrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); addrsrv.sin_family=AF_INET; addrsrv.sin_port=htons(6000); char recvBuf[100]; char sendBuf[100]; char tempBuf[200]; int len=sizeof(SOCKADDR); while(1) { printf("please input data:\n"); gets(sendBuf); sendto(sockClient,sendBuf,strlen(sendBuf)+1,0, (SOCKADDR*)&addrsrv,len); //等待服務器端的回應 recvfrom(sockClient,recvBuf,100,0, (SOCKADDR*)&addrsrv,&len); if('q'==recvBuf[0]) { sendto(sockClient,"q",strlen("q")+1,0, (SOCKADDR*)&addrsrv,len); printf("chat end!\n"); break; } sprintf(tempBuf,"%s say :%s",inet_ntoa(addrsrv.sin_addr),recvBuf); printf("%s\n",tempBuf); } closesocket(sockClient); WSACleanup(); }