您的位置:首页 > 理论基础 > 计算机网络

剖析网络编程-TCP&UDP

2010-05-14 01:47 666 查看
网络程序的实现可以有很多方式,Windows Socket就是其中一种比较简单的方法。socket是连接应用程序与网络驱动程序的桥梁,socket在应用程序中创建,通过绑定操作与驱动程序建立关系。此后,应用程序送给socket的数据,由socket交给驱动程序向网络上发送出去。计算机从网络上收到与该socket绑定的IP地址和端口号相关的数据后,由驱动程序交给socket,应用程序便可从该socket中提取接收到的数据。

  在TCP/IP网络应用中,通信的两个进程间相互作用的主要是(client/server)模式,即客户向服务器提出请求,服务器接收到请求后,提供相应的服务。

  下面通过一个简单的实例来讲述基于TCP的socket编程的通信流程。其中服务器端程序实现代码TCPSrv.cpp如下:

  Server

 1//#include <windows.h>
 2#include <Winsock2.h>
 3#include <stdio.h>
 4
 5void main()
 6{
 7    //加载套接字库
 8    WORD wVersionRequested;
 9      WSADATA wsaData;
10    int err;
11
12    wVersionRequested = MAKEWORD(1, 1);
13    err = WSAStartup(wVersionRequested, &wsaData);
14    if( err != 0 )
15    {
16        return;
17    }
18    if( LOBYTE(wsaData.wVersion) != 1  ||
19        HIBYTE(wsaData.wVersion) != 1)
20    {
21        WSACleanup();
22        return;
23    }
24
25    //创建用于监听的套接字
26    SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
27    SOCKADDR_IN addSrv;
28    addSrv.sin_addr.S_un.S_addr = htonl( INADDR_ANY );
29    addSrv.sin_family = AF_INET;
30    addSrv.sin_port = htons(6000);
31
32    //绑定套接字
33    bind(sockSrv, (SOCKADDR*)&addSrv, sizeof(SOCKADDR));
34    //将套接字设为监听模式,准备接收客户请求
35    listen(sockSrv, 5);
36
37    SOCKADDR_IN addClient;
38    int len = sizeof(SOCKADDR);
39
40    while(1)
41    {
42        //等待客户请求
43        SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addClient, &len);
44        char sendBuf[1000];
45        sprintf(sendBuf, "Welcome %s to http://www.cnblogs.com/lantionzy", inet_ntoa(addClient.sin_addr));
46        //发送数据
47        send(sockConn, sendBuf, strlen(sendBuf)+1, 0);
48        
49        char receiveBuf[1000];
50        //接收数据
51        recv(sockConn, receiveBuf, 1000, 0);
52        //打印接收的数据
53        printf("%s/n", receiveBuf);
54        //关闭套接字
55        closesocket(sockConn);
56    }
57}


在这段代码中,首先定义了一个WORD类型的变量:wVersionRequested,用来保存WinSocl库的版本号,接着调用 MAKEWORD宏创建一个包含了请求版本号的WORD值,之后调用WSAStartup函数加载套接字库,如果其返回值不等于0 ,则程序退出。接下来判断wsaData.wVersion的低字节和高字节是否都等于1,如果不是我们请求的版本,那么调用WSACleanup函数终止对Winsock库的使用并返回。


  加载套接字库后,就可以按照一定流程来编写实现代码了:

  1、创建套接字(socket)

  利用socket函数创建套接字,对于它来说,第一个参数只能是AF_INET或(PF_INET);本例是基于TCP协议的网络程序,需要创建的是流式套接字,因此将socket函数第二个参数设置为SOCK_STREAM;将其第三个参数指定为0。这样该函数将根据地址格式和套接字类别,自动选择一个合适的协议。

  2、将套接字绑定到一个本地地址和端口上(bind)

  在SOCKADDR_IN结构体中,除了 sa_family成员外,其他成员都是按网络字节顺序表示的。因此使用htonl函数将INADDR_ANY值转换为网络字节顺序。调用bind函把套接字sockSrv绑定到本地地址和指定端口上。其第一个参数为要绑定的套接字,第二个需要一个指针,可以用取地址符来实现,并且addrSrv变量是 SOCKADDR_IN结构体类型,而这里需要的是SOCKADDR*类型,所以要进行强制转换。第三个参数是指定地址结构的大小,可以利用sizeof 操作符来获取。

  3、将套接字设为监听模式(listen),准备接收客户请求。其中listen函数第二个参数是指等待连接队列的最大长度。

  4、等待客户请求的到来;当请求到来后,接受连接请求,返回一个新的对应于此连接的套接字(accept)。

  接下来,需要调用accept函数等待并接受客户的连接请求。因为作为服务器端,它需要不断的等待客户端的连接请求的到来,所以设计成一个死循环。当客户端有请求时,该函数接受请求建立连接,同时返回一个相对于当前这个新连接的一个套接字描述符,保存于sockConn变量中,然后利用这个套接字就可以与客户端进行通信了,而我们先前的套接字仍继续监听客户端的连接请求。

5、用返回的套接字和客户端进行通信(send/recv)

  可以调用send函数向客户端发送数据,注意这里使用的套接字是已建立连接的那个套接字:sockConn,而不是用于监听的那个套接字:addrSrv。使用recv函数从客户端接收数据。

  6、返回等待另一个客户请求

  7、关闭套接字

  上面实现的服务器端的程序,下面是客户端程序实现代码TCPClient.cpp:

  Client

 1//#include <windows.h>
 2#include <Winsock2.h>
 3#include <stdio.h>
 4
 5int main()
 6{
 7    WORD wVersionRequested;
 8      WSADATA wsaData;
 9    int err;
10
11    wVersionRequested = MAKEWORD(1, 1);
12    err = WSAStartup(wVersionRequested, &wsaData);
13    if( err != 0 )
14    {
15        return -1;
16    }
17    if( LOBYTE(wsaData.wVersion) != 1  ||
18        HIBYTE(wsaData.wVersion) != 1)
19    {
20        WSACleanup();
21        return -1;
22    }
23
24    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
25    SOCKADDR_IN addSrv;
26    addSrv.sin_addr.S_un.S_addr = inet_addr( "127.0.0.1" );
27    addSrv.sin_family = AF_INET;
28    addSrv.sin_port = htons(6000);
29
30    connect(sockClient, (SOCKADDR*)&addSrv, sizeof(SOCKADDR)); 
31    
32    char receiveBuf[1000];
33    recv(sockClient, receiveBuf, 1000, 0);
34    printf("%s/n", receiveBuf);
35
36    send(sockClient, "This is lantionzy", strlen("This is lantionzy")+1, 0);
37    closesocket(sockClient);
38
39    WSACleanup();
40}


  对于客户端来说,它不需要邦定,可以直接连接服务器端。

  首先运行服务器端程序,然后再运行客户端程序,可以看到客户端收到了服务器端返回的信息:Welcome 127.0.0.1 to http://www.cppblog.com/lantionzy,而服务器端收到了客户端发送的信息:This is lantionzy。

2。UDP

新建一个空的Win32 Console Application类型的工程,在其中添加实现基于UDP的服务器端程序的代码UDPSrv.cpp:

 1#include <Winsock2.h>
 2#include <stdio.h>
 3
 4void main()
 5{
 6    WORD wVersionRequested;
 7    WSADATA wsaData;
 8    int err;
 9    
10    wVersionRequested = MAKEWORD( 1, 1 );
11    
12    err = WSAStartup( wVersionRequested, &wsaData );
13    if ( err != 0 ) 
14    {
15        return;
16    }
17    
18
19    if ( LOBYTE( wsaData.wVersion ) != 1 ||
20             HIBYTE( wsaData.wVersion ) != 1 ) 
21    {
22        WSACleanup( );
23        return; 
24    }
25
26    SOCKET sockSrv=socket(AF_INET,SOCK_DGRAM,0);
27    SOCKADDR_IN addrSrv;
28    addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
29    addrSrv.sin_family=AF_INET;
30    addrSrv.sin_port=htons(12345);
31
32    bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
33
34    int ret=WSAGetLastError();
35    printf("%d/n",ret);
36
37    SOCKADDR_IN addrClient;
38    int len=sizeof(SOCKADDR);
39    char recvBuf[1000];
40
41    memset(recvBuf,0,1000);
42
43    recvfrom(sockSrv,recvBuf,1000,0,(SOCKADDR*)&addrClient,&len);
44        ret=WSAGetLastError();
45    printf("%d/n",ret);
46    printf("%s/n",recvBuf);
47
48    closesocket(sockSrv);
49    WSACleanup();
50}


  新建另外一个空的Win32 Console Application类型的工程,在其中添加实现基于UDP的客户端程序的代码UDPClient.cpp:

 1#include <Winsock2.h>
 2#include <stdio.h>
 3
 4void main()
 5{
 6    WORD wVersionRequested;
 7    WSADATA wsaData;
 8    int err;
 9    
10    wVersionRequested = MAKEWORD( 1, 1 );
11    
12    err = WSAStartup( wVersionRequested, &wsaData );
13    if ( err != 0 ) 
14    {
15        return;
16    }
17    
18
19    if ( LOBYTE( wsaData.wVersion ) != 1 ||
20             HIBYTE( wsaData.wVersion ) != 1 ) 
21    {
22        WSACleanup( );
23        return; 
24    }
25
26    SOCKET sockClient=socket(AF_INET,SOCK_DGRAM,0);
27    SOCKADDR_IN addrSrv;
28    addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
29    addrSrv.sin_family=AF_INET;
30    addrSrv.sin_port=htons(12345);
31
32    sendto(sockClient,"Hello",strlen("Hello")+1,0,
33        (SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
34
35    closesocket(sockClient);
36    WSACleanup();
37}


  对于基于UDP的服务器来说,就是一个客户端,不用建立监听或者说建立连接,直接调用recvfrom函数接收数据。注意,在编写基于 UDP的网络程序时,在接收数据时,使用的是recvfrom函数。而前面基于TCP的服务器端程序接收数据时使用的是recv函数。

  客户端也可以直接发送数据,这里需要调用sendto函数而不是send函数。

  运行示例中的服务器端程序和客户端程序,服务器端收到一条信息:Hello。这里服务器端作为接收端,客户端作为发送端。

3.注意的地方

针对“剖析网络编程(1)-- 基于TCP的的网络应用程序”和“剖析网络编程(2)-- 基于UDP的的网络应用程序”的示例程序,说明几个用VC++基于TCP/UDP网络编程应注意的几个地方:

  1、基于TCP和基于UDP的网络应用程序在发送和接收数据时使用的函数是不一样的:前者使用send和recv,后者使用sendto和recvfrom。

  2、由于程序中使用了Winsock库中的函数,这里需要为程序链接相应的.lib文件:ws2_32.lib。方法是:

  a.对于VC++6.0用户:选择[Project/Setting...]菜单项,选择Link选项卡,然后在Object/Library modules编辑框中添加ws2_32.lib。注意文件名之间有空格。

  b.对于VS2005/2008/2010用户:选择[Project/Properties],选择Link选项卡,然后在input选项下的Additional Dependencies内输入待加入的lib库文件ws2_32.lib。

  c.代码中添加#pragma    comment(lib,"ws2_32.lib")

  3、我们知道,Windows网络编程至少需要两个头文件:winsock2.h和windows.h,而在WinSock2.0之前还存在一个老版本的winsock.h。正是这三个头文件的包含顺序,导致了问题的出现。

  先让我们看看winsock2.h的内容,在文件开头有如下宏定义:

#ifndef _WINSOCK2API_
#define _WINSOCK2API_
#define _WINSOCKAPI_   /* Prevent inclusion of winsock.h in windows.h */


  _WINSOCK2API_很容易理解,这是最常见的防止头文件重复包含的保护措施。_WINSOCKAPI_的定义则是为了阻止对老文件 winsock.h的包含,即是说,如果用户先包含了winsock2.h就不允许再包含winsock.h了,否则会导致类型重复定义。这是怎样做到的呢?很简单,因为winsock.h的头部同样存在如下的保护措施:

#ifndef _WINSOCKAPI_
#define _WINSOCKAPI_


  接着看winsock2.h,在上述内容之后紧跟着如下宏指令:

/*
* Pull in WINDOWS.H if necessary
*/
#ifndef _INC_WINDOWS
#include <windows.h>
#endif /* _INC_WINDOWS */


  其作用是如果用户没有包含windows.h(_INC_WINDOWS在windows.h中定义)就自动包含它,以定义WinSock2.0所需的类型和常量等。

  现在切换到windows.h,查找winsock,我们会惊奇的发现以下内容:

#ifndef WIN32_LEAN_AND_MEAN
#include <cderr.h>
#include <dde.h>
#include <ddeml.h>
#include <dlgs.h>
#ifndef _MAC
#include <lzexpand.h>
#include <mmsystem.h>
#include <nb30.h>
#include <rpc.h>
#endif
#include <shellapi.h>
#ifndef _MAC
#include <winperf.h> 
#if(_WIN32_WINNT >= 0x0400)
#include <winsock2.h>
#include <mswsock.h>
#else
#include <winsock.h>
#endif /* _WIN32_WINNT >=   0x0400 */
#endif
#endif /* WIN32_LEAN_AND_MEAN */


  windows.h会反向包含winsock2.h或者winsock.h!相互间的包含便是万恶之源!下面看看问题具体是怎么发生的。

  错误情形1:我们在自己的工程中先包含winsock2.h再包含windows.h,如果WIN32_LEAN_AND_MEAN未定义且 _WIN32_WINNT大于或等于0x400,那么windows.h会在winsock2.h开头被自动引入,而windows.h又会自动引入 mswsock.h,此时,mswsock.h里所用的socket类型还尚未定义,因此会出现类型未定义错误。

  错误情形2:先包含windows.h再包含winsock2.h,如果WIN32_LEAN_AND_MEAN未定义且_WIN32_WINNT未定义或者其版本号小于0x400,那么windows.h会自动导入旧有的winsock.h,这样再当winsock2.h被包含时便会引起重定义。

  这里要说明的是,宏WIN32_LEAN_AND_MEAN的作用是减小win32头文件尺寸以加快编译速度,一般由AppWizard在stdafx.h 中自动定义。_WIN32_WINNT的作用是开启高版本操作系统下的特殊函数,比如要使用可等待定时器(WaitableTimer),就得要求 _WIN32_WINNT的值大于或等于0x400。因此,如果你没有遇到上述两个问题,很可能是你没有在这些条件下进行网络编程。

  问题还没有结束,要知道除了VC自带windows库文件外,MS的Platform SDK也含有这些头文件。我们很可能发现在之前能够好好编译的程序在改变了windows头文件包含路径后又出了问题。因为Platform SDK中的windows.h与VC自带的文件存在差异。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: