C語言實現網絡聊天程序的設計與實現(基於TCP協議)

 

目錄

        TCP/IP協議介紹

        TCP/IP協議與WinSock網絡編程接口的關係

        WinSock編程簡單流程

         VC中socket編程

             ·服務器實現

             ·客戶端實現

        測試結果

 

 

 

 

TCP/IP協議介紹

        TCP/IP協議包含的範圍非常的廣,它是一種四層協議,包含了各種硬件、軟件需求的定義。TCP/IP協議確切的說法應該是TCP/UDP/IP協議。UDP協議(User Datagram Protocol 用戶數據報協議),是一種保護消息邊界的,不保障可靠數據的傳輸。TCP協議(Transmission Control Protocol 傳輸控制協議),是一種流傳輸的協議。他提供可靠的、有序的、雙向的、面向連接的傳輸。

        保護消息邊界,就是指傳輸協議把數據當作一條獨立的消息在網上傳輸,接收端只能接收獨立的消息。也就是說存在保護消息邊界,接收端一次只能接收發送端發出的一個數據包。

        而面向流則是指無保護消息邊界的,如果發送端連續發送數據,接收端有可能在一次接收動作中,會接收兩個或者更多的數據包。

        舉例來說,假如,我們連續發送三個數據包,大小分別是2k、4k、8k,這三個數據包都已經到達了接收端的網絡堆棧中,如果使用UDP協議,不管我們使用多大的接收緩衝區去接收數據,我們必須有三次接收動作,才能夠把所有的數據包接收完。而使用TCP協議,我們只要把接收的緩衝區大小設置在14k以上,我們就能夠一次把所有的數據包接收下來,只需要有一次接收動作。

        這就是因爲UDP協議的保護消息邊界使得每一個消息都是獨立的。而流傳輸,卻把數據當作一串數據流,它不認爲數據是一個一個的消息。所以有很多人在使用TCP協議通訊的時候,並不清楚TCP是基於流的傳輸,當連續發送數據的時候,他們時常會認爲TCP會丟包。其實不然,因爲當它們使用的緩衝區足夠大時,它們有可能會一次接收到兩個甚至更多的數據包,而很多人往往會忽視這一點,只解析檢查了第一個數據包,而已經接收的其它據包卻被忽略了。

 

TCP/IP協議與WinSock網絡編程接口的關係

        WinSock 並不是一種網絡協議,它只是一個網絡編程接口,也就是說,它不是協議,但是它可以訪問很多種網絡協議,你可以把它當作一些協議的封裝。現在的 WinSock已經基本上實現了與協議無關。你可以使用WinSock來調用多種協議的功能。那麼,WinSock和TCP/IP協議到底是什麼關係呢?實際上,WinSock就是TCP/IP協議的一種封裝,你可以通過調用WinSock的接口函數來調用TCP/IP的各種功能.例如我想用TCP/IP 協議發送數據,你就可以使用WinSock的接口函數Send()來調用TCP/IP的發送數據功能,至於具體怎麼發送數據,WinSock已經幫你封裝好了這種功能。

 

WinSock編程簡單流程

        WinSock編程分爲服務器端和客戶端兩部分,TCP服務器端的大體流程如下:

        對於任何基於WinSock的編程首先必須要初始化WinSock DLL庫。

int WSAStarup( WORD wVersionRequested,LPWSADATA lpWsAData )。

        wVersionRequested是我們要求使用的WinSock的版本。

        調用這個接口函數可以初始化WinSock 。

        然後必須創建一個套接字(Socket)。

SOCKET Socket(int af,int type,int protocol);

        套接字可以說是WinSock通訊的核心。WinSock通訊的所有數據傳輸,都是通過套接字來完成的,套接字包含了兩個信息,一個是IP地址,一個是Port端口號,使用這兩個信息,就可以確定網絡中的任何一個通訊節點。

        當調用了Socket()接口函數創建了一個套接字後,必須把套接字與你需要進行通訊的地址建立聯繫,可以通過綁定函數bind來實現這種聯繫。

int bind(SOCKET s,const struct sockaddr FAR* name,int namelen) ;

struct sockaddr_in{

    short sin_family ;

    u_short sin_port;

    struct in_addr sin_addr ;

    char sin_sero[8] ;

}

        就包含了需要建立連接的本地的地址,包括地址族、IP和端口信息。sin_family字段必須把它設爲AF_INET,這是告訴WinSock使用的是IP地址族。sin_port就是要用來通訊的端口號。sin_addr就是要用來通訊的IP地址信息。

        在這裏,必須還得提一下有關'大頭(big-endian)'小頭(little-endian)'。因爲各種不同的計算機處理數據時的方法是不一樣的,Intel X86處理器上是用'小頭'形式來表示多字節的編號,就是把低字節放在前面,把高字節放在後面,而互聯網標準卻正好相反,所以,必須把主機字節轉換成網絡字節的順序。WinSock API提供了幾個函數。

        把主機字節轉化成網絡字節的函數;

u_long htonl(u_long hostlong);

u_short htons(u_short hostshort);

        把網絡字節轉化成主機字節的函數;

u_long ntohl(u_long netlong);

u_short ntohs(u_short netshort) ;

        這樣,設置IP地址和port端口時,就必須把主機字節轉化成網絡字節後,才能用Bind()函數來綁定套接字和地址。

        當綁定完成之後,服務器端必須建立一個監聽的隊列來接收客戶端的連接請求。

int listen(SOCKET s,int backlog);

        這個函數可以把套接字轉成監聽模式。

        如果客戶端有了連接請求,我們還必須使用

int accept(SOCKET s,struct sockaddr FAR* addr,int FAR* addrlen);

        來接受客戶端的請求。

        現在基本上已經完成了一個服務器的建立,而客戶端的建立的流程則是初始化WinSock,然後創建Socket套接字,再使用

int connect(SOCKET s,const struct sockaddr FAR* name,int namelen) ;

        來連接服務端。

        下面是一個最簡單的創建服務器端和客戶端的例子:

        服務器端的創建:

WSADATA wsd;

SOCKET sListen;

SOCKET sclient;

UINT port = 800;

int iAddrSize;

struct sockaddr_in local , client;

WSAStartup( 0x11 , &wsd );

sListen = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );

local.sin_family = AF_INET;

local.sin_addr = htonl( INADDR_ANY );

local.sin_port = htons( port );

bind( sListen , (struct sockaddr*)&local , sizeof( local ) );

listen( sListen , 5 );

sClient = accept( sListen , (struct sockaddr*)&client , &iAddrSize );

        客戶端的創建:

WSADATA wsd;

SOCKET sClient;

UINT port = 800;

char szIp[] = "127.0.0.1";

int iAddrSize;

struct sockaddr_in server;

WSAStartup( 0x11 , &wsd );

sClient = Socket ( AF_INET , SOCK_STREAM , IPPOTO_IP );

server.sin_family = AF_INET;

server.sin_addr = inet_addr( szIp );

server.sin_port = htons( port );

connect( sClient , (struct sockaddr*)&server , sizeof( server ) );

        當服務器端和客戶端建立連接以後,無論是客戶端,還是服務器端都可以使用

int send( SOCKET s,const char FAR* buf,int len,int flags);

int recv( SOCKET s,char FAR* buf,int len,int flags);

        函數來接收和發送數據,因爲,TCP連接是雙向的。

        當要關閉通訊連接的時候,任何一方都可以調用

int shutdown(SOCKET s,int how);

        來關閉套接字的指定功能,再調用

int closeSocket(SOCKET s) ;

        來關閉套接字句柄,這樣一個通訊過程就算完成了。

        可以參考教材計算機網絡(第6版)295頁圖6-32所示的系統調用使用順序:

        注意:上面的代碼沒有任何檢查函數返回值,如果你作網絡編程就一定要檢查任何一個WinSock API函數的調用結果,因爲很多時候函數調用並不一定成功。上面介紹的函數,返回值類型是int的話,如果函數調用失敗的話,返回的都是SOCKET_ERROR。

 

VC中socket編程

 

        ·服務器實現

        服務器端編程的步驟:

        1:加載套接字庫,創建套接字(WSAStartup()/socket());

        2:綁定套接字到一個IP地址和一個端口上(bind());

        3:將套接字設置爲監聽模式等待連接請求(listen());

        4:請求到來後,接受連接請求,返回一個新的對應於此次連接的套接字(accept());

        5:用返回的套接字和客戶端進行通信(send()/recv());

        6:返回,等待另一連接請求;

        7:關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup())。

        服務器端代碼如下:

#include <stdio.h>
#include <Winsock2.h>
void main()
{
	WORD wVersionRequested;
	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_STREAM,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));
	
	listen(sockSrv,5);
	
	SOCKADDR_IN addrClient;
	int len=sizeof(SOCKADDR);
	while(1){
		SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);
		char sendBuf[50];
		sprintf(sendBuf,"Welcome %s to here!",inet_ntoa(addrClient.sin_addr));
		send(sockConn,sendBuf,strlen(sendBuf)+1,0);
		char recvBuf[50];
		recv(sockConn,recvBuf,50,0);
		printf("%s\n",recvBuf);
		closesocket(sockConn);
	}
}

 

        ·客戶端實現

        客戶端編程的步驟:

        1:加載套接字庫,創建套接字(WSAStartup()/socket());

        2:向服務器發出連接請求(connect());

        3:和服務器端進行通信(send()/recv());

        4:關閉套接字,關閉加載的套接字庫(closesocket()/WSACleanup())。

        客戶端的代碼如下:

#include <stdio.h>
#include <Winsock2.h>
void main()
{
	WORD wVersionRequested;
	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);
	
	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);
	connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
	send(sockClient,"hello",strlen("hello")+1,0);
	char recvBuf[50];
	recv(sockClient,recvBuf,50,0);
	printf("%s\n",recvBuf);
	closesocket(sockClient);
	WSACleanup();
}

測試結果

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章