首页 > 代码库 > 理解网络编程和套接字

理解网络编程和套接字

编写“Hello world!”服务器端

服务器端(server)是能够受理连接请求的程序。下面构建服务器端以验证之前提到的函数调用过程,该服务器端收到连接请求后向请求者返回“Hello world!”答复。除各种函数的调用顺序外,我们还未涉及任何实际编程。因此,阅读代码时请重点关注套接字相关函数的调用过程,不必理解全部示例。

hello_server.c
 

  1. 1.  #include <stdio.h
  2. 2.  #include <stdlib.h
  3. 3.  #include <string.h
  4. 4.  #include <unistd.h
  5. 5.  #include <arpa/inet.h
  6. 6.  #include <sys/socket.h
  7. 7.  void error_handling(char *message);  
  8. 8.    
  9. 9.  int main(int argc, char *argv[])  
  10. 10. {  
  11. 11.     int serv_sock;  
  12. 12.     int clnt_sock;  
  13. 13.   
  14. 14.     struct sockaddr_in serv_addr;  
  15. 15.     struct sockaddr_in clnt_addr;  
  16. 16.     socklen_t clnt_addr_size;  
  17. 17.   
  18. 18.     char message[]="Hello World!";  
  19. 19.       
  20. 20.     if(argc!=2)  
  21. 21.     {  
  22. 22.         printf("Usage : %s <port>\n", argv[0]);  
  23. 23.         exit(1);  
  24. 24.     }  
  25. 25.       
  26. 26.     serv_sock=socket(PF_INET, SOCK_STREAM, 0);  
  27. 27.     if(serv_sock == -1)  
  28. 28.         error_handling("socket() error");  
  29. 29.       
  30. 30.     memset(&serv_addr, 0, sizeof(serv_addr));  
  31. 31.     serv_addr.sin_family=AF_INET;  
  32. 32.     serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);  
  33. 33.     serv_addr.sin_port=htons(atoi(argv[1]));  
  34. 34.       
  35. 35.     if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1)  
  36. 36.         error_handling("bind() error");  
  37. 37.       
  38. 38.     if(listen(serv_sock, 5)==-1)  
  39. 39.         error_handling("listen() error");  
  40. 40.       
  41. 41.     clnt_addr_size=sizeof(clnt_addr);   
  42. 42.     clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);  
  43. 43.     if(clnt_sock==-1)  
  44. 44.         error_handling("accept() error");  
  45. 45.       
  46. 46.     write(clnt_sock, message, sizeof(message));  
  47. 47.     close(clnt_sock);  
  48. 48.     close(serv_sock);  
  49. 49.     return 0;  
  50. 50. }  
  51. 51.   
  52. 52. void error_handling(char *message)  
  53. 53. {  
  54. 54.     fputs(message, stderr);  
  55. 55.     fputc(‘\n‘, stderr);  
  56. 56.     exit(1);  
  57. 57. } 

第26行:调用socket函数创建套接字。

第35行:调用bind函数分配IP地址和端口号。

第38行:调用listen函数将套接字转为可接收连接状态。

第42行:调用accept函数受理连接请求。如果在没有连接请求的情况下调用该函数,则不会返回,直到有连接请求为止。

第46行:稍后将要介绍的write函数用于传输数据,若程序经过第42行代码执行到本行,则说明已经有了连接请求。

编译并运行以上示例,创建等待连接请求的服务器端。目前不必详细分析源代码,只需确认之前4个函数调用过程。稍后将讲解上述示例中调用的write函数。下面讨论如何编写向服务器端发送连接请求的客户端。

构建打电话套接字

服务器端创建的套接字又称为服务器端套接字或监听(listening)套接字。接下来介绍的套接字是用于请求连接的客户端套接字。客户端套接字的创建过程比创建服务器端套接字简单,因此直接进行讲解。

还未介绍打电话(请求连接)的函数,因为其调用的是客户端套接字,如下所示。
 

  1. #include <sys/socket.h
  2. int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen); 

成功时返回0,失败时返回-1。

客户端程序只有“调用socket函数创建套接字”和“调用connect函数向服务器端发送连接请求”这两个步骤,因此比服务器端简单。下面给出客户端,查看以下两项内容:第一,调用socket函数和connect函数;第二,与服务器端共同运行以收发字符串数据。

hello_client.c
 

  1. 1.  #include <stdio.h
  2. 2.  #include <stdlib.h
  3. 3.  #include <string.h
  4. 4.  #include <unistd.h
  5. 5.  #include <arpa/inet.h
  6. 6.  #include <sys/socket.h
  7. 7.  void error_handling(char *message);  
  8. 8.    
  9. 9.  int main(int argc, char* argv[])  
  10. 10. {  
  11. 11.     int sock;  
  12. 12.     struct sockaddr_in serv_addr;  
  13. 13.     char message[30];  
  14. 14.     int str_len;  
  15. 15.       
  16. 16.     if(argc!=3)  
  17. 17.     {  
  18. 18.         printf("Usage : %s <IP<port>\n", argv[0]);  
  19. 19.         exit(1);  
  20. 20.     }  
  21. 21.       
  22. 22.     sock=socket(PF_INET, SOCK_STREAM, 0);  
  23. 23.     if(sock == -1)  
  24. 24.         error_handling("socket() error");  
  25. 25.       
  26. 26.     memset(&serv_addr, 0, sizeof(serv_addr));  
  27. 27.     serv_addr.sin_family=AF_INET;  
  28. 28.     serv_addr.sin_addr.s_addr=inet_addr(argv[1]);  
  29. 29.     serv_addr.sin_port=htons(atoi(argv[2]));  
  30. 30.           
  31. 31.     if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)  
  32. 32.         error_handling("connect() error!");  
  33. 33.       
  34. 34.     str_len=read(sock, message, sizeof(message)-1);  
  35. 35.     if(str_len==-1)  
  36. 36.         error_handling("read() error!");  
  37. 37.       
  38. 38.     printf("Message from server : %s \n", message);   
  39. 39.     close(sock);  
  40. 40.     return 0;  
  41. 41. }  
  42. 42.   
  43. 43. void error_handling(char *message)  
  44. 44. {  
  45. 45.     fputs(message, stderr);  
  46. 46.     fputc(‘\n‘, stderr);  
  47. 47.     exit(1); 

第22行:创建套接字,但此时套接字并不马上分为服务器端和客户端。如果紧接着调用bind、listen函数,将成为服务器端套接字;如果调用connect函数,将成为客户端套接字。

第31行:调用connect函数向服务器端发送连接请求。

这样就编好了服务器端和客户端,相信各位会产生好多疑问(实际上不懂的内容比知道的更多)。接下来的几章将进行解答,请不要着急。

在Linux平台下运行

虽未另行说明,但上述两个示例应在Linux环境中编译并执行。接下来将简单介绍Linux下的C语言编译器——GCC(GNU Compiler Collection,GNU编译器集合)。下面是对hello_server.c示例进行编译的命令。
 

  1. gcc hello_server.c –o hserver 

编译hello_server.c文件并生成可执行文件hserver。

该命令中的-o是用来指定可执行文件名的可选参数,因此,编译后将生成可执行文件hserver。可如下执行此项命令。
 

  1. ./hserver 

运行当前目录下的hserver文件。

了解编译和运行相关知识的确多多益善,但学习本书只需掌握基本用法。接下来运行程序。服务器端需要在运行时接收客户端的连接请求,因此先运行服务器端。

运行结果:hello_server.c
 

  1. root@my_linux:/tcpip# gcc hello_server.c -o hserver  
  2. root@my_linux:/tcpip# ./hserver 9190 

正常情况下程序将停留在此状态,因为服务器端调用的accept函数还未返回。接下来运行客户端。

运行结果:hello_client.c
 

  1. root@my_linux:/tcpip# gcc hello_client.c -o hclient  
  2. root@my_linux:/tcpip# ./hclient 127.0.0.1 9190  
  3. Message from server: Hello World!  
  4. root@my_linux:/tcpip# 

由此查看客户端消息传输过程。同时发现,完成消息传输后,服务器端和客户端都停止运行。执行过程中输入的127.0.0.1是运行示例用的计算机(本地计算机)的IP地址。如果在同一台计算机中同时运行服务器端和客户端,将采用这种连接方式。但如果服务器端与客户端在不同计算机中运行,则应采用服务器端所在计算机的IP地址。

再次运行程序前需等待

上面的服务器端无法立即重新运行。如果想再次运行,则需要更改之前输入的端口号9190。后面会详细讲解其原因,现在不必对此感到意外。

1.2 基于Linux的文件操作(1)

讨论套接字的过程中突然谈及文件也许有些奇怪。但对Linux而言,socket操作与文件操作没有区别,因而有必要详细了解文件。在Linux世界里,socket也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件I/O的相关函数。Windows则与Linux不同,是要区分socket和文件的。因此在Windows中需要调用特殊的数据传输相关函数。

底层文件访问(Low-Level File Access)和文件描述符(File Descriptor)

即使看到“底层”二字,也会有读者臆测其难以理解。实际上,“底层”这个表达可以理解为“与标准无关的操作系统独立提供的”。稍后讲解的函数是由Linux提供的,而非ANSI标准定义的函数。如果想使用Linux提供的文件I/O函数,首先应该理解好文件描述符的概念。

此处的文件描述符是系统分配给文件或套接字的整数。实际上,学习C语言过程中用过的标准输入输出及标准错误在Linux中也被分配表1-1中的文件描述符。

表1-1 分配给标准输入输出及标准错误的文件描述符

文件描述符

对象

0

标准输入:Standard Input

1

标准输出:Standard Output

2

标准错误:Standard Error

文件和套接字一般经过创建过程才会被分配文件描述符。而表1-1中的3种输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。稍后将详细讲解其使用方法及含义。

知识补给站 文件描述符(文件句柄)

学校附近有个服务站,只需打个电话就能复印所需论文。服务站有位常客叫英秀,他每次都要求复印同一篇论文的一部分内容。

“大叔您好!请帮我复印一下《关于随着高度信息化社会而逐步提升地位的触觉、知觉、思维、性格、智力等人类生活质量相关问题特性的人类学研究》这篇论文第26页到第30页。”

这位同学每天这样打好几次电话,更雪上加霜的是语速还特别慢。终于有一天大叔说:

“从现在开始,那篇论文就编为第18号!你就说帮我复印18号论文26页到30页!”

之后英秀也是只复印超过50字标题的论文,大叔也会给每篇论文分配无重复的新号(数字)。这才不会头疼于与英秀的对话,且不影响业务。

该示例中,大叔相当于操作系统,英秀相当于程序员,论文号相当于文件描述符,论文相当于文件或套接字。也就是说,每当生成文件或套接字,操作系统将返回分配给它们的整数。这个整数将成为程序员与操作系统之间良好沟通的渠道。实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已。

文件描述符有时也称为文件句柄,但“句柄”主要是Windows中的术语。因此,本书中如果涉及Windows平台将使用“句柄”,如果是Linux平台则用“描述符”。

打开文件

首先介绍打开文件以读写数据的函数。调用此函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)。
 

  1. #include <sys/types.h> 
  2. #include <sys/stat.h> 
  3. #include <fcntl.h> 
  4. int open(const char *path, int flag); 

成功时返回文件描述符,失败时返回-1。

path 文件名的字符串地址。

flag 文件打开模式信息。

表1-2是此函数第二个参数flag可能的常量值及含义。如需传递多个参数,则应通过位或运算(OR)符组合并传递。

表1-2 文件打开模式

打开模式

含义

O_CREAT

必要时创建文件

O_TRUNC

删除全部现有数据

O_APPEND

维持现有数据,保存到其后面

O_RDONLY

只读打开

O_WRONLY

只写打开

O_RDWR

读写打开

稍后将给出此函数的使用示例。接下来先介绍关闭文件和写文件时调用的函数。

1.3 基于Windows平台的实现

Windows套接字(以下简称Winsock)大部分是参考BSD系列UNIX套接字设计的,所以很多地方都跟Linux套接字类似。因此,只需要更改Linux环境下编好的一部分网络程序内容,就能在Windows平台下运行。本书也会同时讲解Linux和Windows两大平台,这不会给大家增加负担,反而会减轻压力。

同时学习Linux和Windows的原因

大多数项目都在Linux系列的操作系统下开发服务器端,而多数客户端是在Windows平台下开发的。不仅如此,有时应用程序还需要在两个平台之间相互切换。因此,学习套接字编程的过程中,有必要兼顾Windows和Linux两大平台。另外,这两大平台下的套接字编程非常类似,如果把其中相似的部分放在一起讲解,将大大提高学习效率。这会不会增加学习负担?一点也不。只要理解好其中一个平台下的网络编程方法,就很容易通过分析差异掌握另一平台。

为Windows套接字编程设置头文件和库

为了在Winsock基础上开发网络程序,需要做如下准备。

导入头文件winsock2.h。

链接ws2_32.lib库。

首先介绍项目中链接ws2_32.lib库的方法。我用的环境是Visual Studio 2008版本,接下来的讲解同样适用于更高版本的开发环境,不必因版本不同而感到困惑。打开项目的“属性”页,选择“配置属性”→“输入”→“附加依赖项”,如图1-1所示。当然,也可以通过快捷键Alt+F7打开“属性页”。

 

 

接下来需要在图1-1的“附加依赖项”右边空白处直接写入ws2_32.lib。也可以通过点击空白处右边的按钮弹出如图1-2所示的对话框,并填写库名。

 

 

设置库的工作到此结束。现在只需要在源文件中添加头文件,即可调用Winsock相关函数。

附加依赖项窗口位置可能不同

根据VC++版本号的不同,图1-2中的附加依赖项窗口位置可能不同。一般可通过以下两种路径找到附加依赖项。

快捷键Alt+F7→“配置属性”→“输入”→“附加依赖项”

快捷键Alt+F7→“配置属性”→“链接器”→“输入”→“附加依赖项”

Winsock的初始化

进行Winsock编程时,首先必须调用WSAStartup函数,设置程序中用到的Winsock版本,并初始化相应版本的库。
 

  1. #include <winsock2.h
  2. int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData); 

成功时返回0,失败时返回非零的错误代码值。

wVersionRequested 程序员要用的Winsock版本信息。

lpWSAData    WSADATA结构体变量的地址值。

有必要给出上述两个参数的详细说明。先说第一个,Winsock中存在多个版本,应准备WORD类型的(WORD是通过typedef声明定义的unsigned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested。若版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。

如前所述,高8位为副版本号,低8位为主版本号,以此进行传递。本书主要使用2.2版本,故应传递0x0202。不过,以字节为单位手动构造版本信息有些麻烦,借助MAKEWORD宏函数则能轻松构建WORD型版本信息。

MAKEWORD(1, 2);://主版本为1,副版本为2,返回0x0201。

MAKEWORD(2, 2);://主版本为2,副版本为2,返回0x0202。

接下来讲解第二个参数lpWSADATA,此参数中需传入WSADATA型结构体变量地址(LPWSADATA是WSADATA的指针类型)。调用完函数后,相应参数中将填充已初始化的库信息。虽无特殊含义,但为了调用函数,必须传递WSADATA结构体变量地址。下面给出WSAStartup函数调用过程,这段代码几乎已成为Winsock编程的公式。
 

  1. int main(int argc, char* argv[])  
  2. {  
  3. WSADATA wsaData;  
  4. . . . .  
  5. if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  
  6.     ErrorHandling("WSAStartup() error!");  
  7. . . . .  
  8. return 0;  

前面已经介绍了Winsock相关库的初始化方法,接下来讲解如何注销该库——利用下面给出的函数。
 

  1. #include <winsock2.h
  2. int WSACleanup(void); 

成功时返回0,失败时返回SOCKET_ERROR。

调用该函数时,Winsock相关库将归还Windows操作系统,无法再调用Winsock相关函数。从原则上讲,无需再使用Winsock函数时才调用该函数,但通常都在程序结束之前调用。

1.4 基于Windows的套接字相关函数及示例(1)

本节介绍的Winsock函数与之前的Linux套接字相关函数相对应。既然只是介绍,就不做详细说明了,目的只在于让各位体会基于Linux和Windows的套接字函数之间的相似性。

基于Windows的套接字相关函数

首先介绍的函数与Linux下的socket函数提供相同功能。稍后讲解返回值类型SOCKET。
 

  1. #include <winsock2.h
  2. SOCKET socket(int af, int type, int protocol); 

成功时返回套接字句柄,失败时返回INVALID_SOCKET。

下列函数与Linux的bind函数相同,调用其分配IP地址和端口号。
 

  1. #include <winsock2.h
  2. int bind(SOCKET s, const struct sockaddr * name, int namelen); 

成功时返回0,失败时返回SOCKET_ERROR。

下列函数与Linux的listen函数相同,调用其使套接字可接收客户端连接。
 

  1. #include <winsock2.h
  2. int listen(SOCKET s,int backlog); 

成功时返回0,失败时返回SOCKET_ERROR。

下列函数与Linux的accept函数相同,调用其受理客户端连接请求。
 

  1. #include <winsock2.h
  2. SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen); 

成功时返回套接字句柄,失败时返回INVALID_SOCKET。

下列函数与Linux的connect函数相同,调用其从客户端发送连接请求。
 

  1. #include <winsock2.h
  2. int connect(SOCKET s, const struct sockaddr * name, int namelen); 

成功时返回0,失败时返回SOCKET_ERROR。

最后这个函数在关闭套接字时调用。Linux中,关闭文件和套接字时都会调用close函数;而Windows中有专门用来关闭套接字的函数。
 

  1. #include <winsock2.h
  2. int closesocket(SOCKET s); 

成功时返回0,失败时返回SOCKET_ERROR。

以上就是基于Windows的套接字相关函数,虽然返回值和参数与Linux函数有所区别,但具有相同功能的函数名是一样的。正是这些特点使跨越两大操作系统平台的网络编程更加简单。

Windows中的文件句柄和套接字句柄

Linux内部也将套接字当作文件,因此,不管创建文件还是套接字都返回文件描述符。之前也通过示例介绍了文件描述符返回及编号的过程。Windows中通过调用系统函数创建文件时,返回“句柄”(handle),换言之,Windows中的句柄相当于Linux中的文件描述符。只不过Windows中要区分文件句柄和套接字句柄。虽然都称为“句柄”,但不像Linux那样完全一致。文件句柄相关函数与套接字句柄相关函数是有区别的,这一点不同于Linux文件描述符。

既然对句柄有了一定理解,接下来再观察基于Windows的套接字相关函数,这将加深各位对SOCKET类型的参数和返回值的理解。的确!这就是为了保存套接字句柄整型值的新数据类型,它由typedef声明定义。回顾socket、listen和accept等套接字相关函数,则更能体会到与Linux中套接字相关函数的相似性。

有些程序员可能会问:“既然Winsock是以UNIX、Linux系列的BSD套接字为原型设计的,为什么不照搬过来,而是存在一定差异呢?”有人认为这是微软为了防止UNIX、Linux服务器端直接移植到Windows而故意为之。从网络程序移植性角度上看,这也是可以理解的。但我有不同意见。从本质上说,两种操作系统内核结构上存在巨大差异,而依赖于操作系统的代码实现风格也不尽相同,连Windows程序员给变量命名的方式也不同于Linux程序员。从各方面考虑,保持这种差异性就显得比较自然。因此我个人认为,Windows套接字与BSD系列的套接字编程方式有所不同是为了保持这种自然差异性。

创建基于Windows的服务器端和客户端

接下来将之前基于Linux的服务器端与客户端示例转化到Windows平台。目前想完全理解这些代码有些困难,我们只需验证套接字相关函数的调用过程、套接字库的初始化与注销过程即可。先介绍服务器端示例。

hello_server_win.c
 

    1. 1.  #include <stdio.h
    2. 2.  #include <stdlib.h
    3. 3.  #include <winsock2.h
    4. 4.  void ErrorHandling(char* message);  
    5. 5.    
    6. 6.  int main(int argc, char* argv[])  
    7. 7.  {  
    8. 8.      WSADATA wsaData;  
    9. 9.      SOCKET hServSock, hClntSock;  
    10. 10.     SOCKADDR_IN servAddr, clntAddr;   
    11. 11.   
    12. 12.     int szClntAddr;  
    13. 13.     char message[]="Hello World!";  
    14. 14.     if(argc!=2)   
    15. 15.     {  
    16. 16.         printf("Usage : %s <port>\n", argv[0]);  
    17. 17.         exit(1);  
    18. 18.     }  
    19. 19.   
    20. 20.     if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)  
    21. 21.         ErrorHandling("WSAStartup() error!");   
    22. 22.       
    23. 23.     hServSock=socket(PF_INET, SOCK_STREAM, 0);  
    24. 24.     if(hServSock==INVALID_SOCKET)  
    25. 25.         ErrorHandling("socket() error");  
    26. 26.   
    27. 27.     memset(&servAddr, 0, sizeof(servAddr));  
    28. 28.     servAddr.sin_family=AF_INET;  
    29. 29.     servAddr.sin_addr.s_addr=htonl(INADDR_ANY);  
    30. 30.     servAddr.sin_port=htons(atoi(argv[1]));  
    31. 31.       
    32. 32.     if(bind(hServSock, (SOCKADDR*) &servAddr, sizeof(servAddr))==SOCKET_ERROR)  
    33. 33.         ErrorHandling("bind() error");   
    34. 34.       
    35. 35.     if(listen(hServSock, 5)==SOCKET_ERROR)  
    36. 36.         ErrorHandling("listen() error");  
    37. 37.   
    38. 38.     szClntAddr=sizeof(clntAddr);   
    39. 39.     hClntSock=accept(hServSock, (SOCKADDR*)&clntAddr,&szClntAddr);  
    40. 40.     if(hClntSock==INVALID_SOCKET)  
    41. 41.         ErrorHandling("accept() error");   
    42. 42.       
    43. 43.     send(hClntSock, message, sizeof(message), 0);  
    44. 44.     closesocket(hClntSock);  
    45. 45.     closesocket(hServSock);  
    46. 46.     WSACleanup();  
    47. 47.     return 0;  
    48. 48. }  
    49. 49.   
    50. 50. void ErrorHandling(char* message)  
    51. 51. {  
    52. 52.     fputs(message, stderr);  
    53. 53.     fputc(‘\n‘, stderr);  
    54. 54.     exit(1);  
    55. 55. } 

第20行:初始化套接字库。

第23、32行:第23行创建套接字,第32行给该套接字分配IP地址与端口号。

第35行:调用listen函数使第23行创建的套接字成为服务器端套接字。

第39行:调用accept函数受理客户端连接请求。

第43行:调用send函数向第39行连接的客户端传输数据。稍后讲解send函数。

第46行:程序终止前注销第20行中初始化的套接字库。

可以看出,除了Winsock库的初始化和注销相关代码、数据类型信息外,其余部分与Linux环境下的示例并无区别。希望各位阅读这部分代码时与之前的Linux服务器端进行逐行比较。接下来介绍与此示例同步的客户端代码。

hello_client_win.c
 

  1. 1.  #include <stdio.h
  2. 2.  #include <stdlib.h
  3. 3.  #include <winsock2.h
  4. 4.  void ErrorHandling(char* message);  
  5. 5.    
  6. 6.  int main(int argc, char* argv[])  
  7. 7.  {  
  8. 8.      WSADATA wsaData;  
  9. 9.      SOCKET hSocket;  
  10. 10.     SOCKADDR_IN servAddr;  
  11. 11.   
  12. 12.     char message[30];  
  13. 13.     int strLen;  
  14. 14.     if(argc!=3)  
  15. 15.     {  
  16. 16.         printf("Usage : %s <IP<port>\n", argv[0]);  
  17. 17.         exit(1);  
  18. 18.     }  
  19. 19.   
  20. 20.     if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)  
  21. 21.         ErrorHandling("WSAStartup() error!");  
  22. 22.       
  23. 23.     hSocket=socket(PF_INET, SOCK_STREAM, 0);  
  24. 24.     if(hSocket==INVALID_SOCKET)  
  25. 25.         ErrorHandling("socket() error");  
  26. 26.       
  27. 27.     memset(&servAddr, 0, sizeof(servAddr));  
  28. 28.     servAddr.sin_family=AF_INET;  
  29. 29.     servAddr.sin_addr.s_addr=inet_addr(argv[1]);  
  30. 30.     servAddr.sin_port=htons(atoi(argv[2]));  
  31. 31.       
  32. 32.     if(connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr))==SOCKET_ERROR)  
  33. 33.         ErrorHandling("connect() error!");  
  34. 34.   
  35. 35.     strLen=recv(hSocket, message, sizeof(message)-1, 0);  
  36. 36.     if(strLen==-1)  
  37. 37.         ErrorHandling("read() error!");  
  38. 38.     printf("Message from server: %s \n", message);   
  39. 39.   
  40. 40.     closesocket(hSocket);  
  41. 41.     WSACleanup();  
  42. 42.     return 0;  
  43. 43. }  
  44. 44.   
  45. 45. void ErrorHandling(char* message)  
  46. 46. {  
  47. 47.     fputs(message, stderr);  
  48. 48.     fputc(‘\n‘, stderr);  
  49. 49.     exit(1);  
  50. 50. } 

第20行:初始化Winsock库。

第23、32行:第23行创建套接字,第32行通过此套接字向服务器端发出连接请求。

第35行:调用recv函数接收服务器发来的数据。稍后讲解该函数。

第41行:注销第20行中初始化的Winsock库。

下面运行以上示例。创建编译项目的过程与各位学习C语言时使用的方法相同,只是增加了设置ws2_32.lib链接库的过程。

运行结果:hello_server_win.c
 

  1. C:\tcpip>hServerWin 9190 

运行过程中,假设可执行文件名为hServerWin.exe。如果运行正常,则与Linux相同,程序进入等待状态。这是因为服务器端调用了accept函数。接着运行客户端,假设客户端的可执行文件名为hClientWin.exe。

运行结果:hello_client_win.c
 

  1. C:\tcpip>hClientWin 127.0.0.1 9190  
  2. Message from server: Hello World! 

基于Windows的I/O函数

Linux中套接字也是文件,因而可以通过文件I/O函数read和write进行数据传输。而Windows中则有些不同。Windows严格区分文件I/O函数和套接字I/O函数。下面介绍Winsock数据传输函数。
 

  1. #include <winsock2.h
  2. int send(SOCKET s, const char * buf, int len, int flags); 

成功时返回传输字节数,失败时返回SOCKET_ERROR。

s 表示数据传输对象连接的套接字句柄值。

buf 保存待传输数据的缓冲地址值。

len 要传输的字节数。

flags 传输数据时用到的多种选项信息。

此函数与Linux的write函数相比,只是多出了最后的flags参数。后续章节中将给出该参数的详细说明,在此之前只需传递0,表示不设置任何选项。但有一点需要注意,send函数并非Windows独有。Linux中也有同样的函数,它也来自于BSD套接字。只不过我在Linux相关示例中暂时只使用read、write函数,为了强调Linux环境下文件I/O和套接字I/O相同。下面介绍与send函数对应的recv函数。
 

  1. #include <winsock2.h
  2. int recv(SOCKET s, const char * buf, int len, int flags); 

成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKET_ERROR。

s 表示数据接收对象连接的套接字句柄值。

buf 保存接收数据的缓冲地址值。

len 能够接收的最大字节数。

flags 接收数据时用到的多种选项信息。

我只是在Windows环境下提前介绍了send、recv函数,以后的Linux示例中也会涉及。请不要误认为Linux中的read、write函数就是对应于Windows的send、recv函数。另外,之前的程序代码中也给出了send、recv函数调用过程,故不再另外给出相关示例。

知识补给站 Windows?Linux?

过去要编写服务器端的话,大部分程序员都会想起Linux或UNIX,因为那时的Windows还被认为是只能给个人使用的操作系统。即使Windows已经开始提供运营服务器端所需环境,但绝大多数网络程序员都会选择Linux和UNIX。不过,随着多媒体数据传输要求的提高,程序员的想法也有了变化。他们会根据服务器端的特点和环境选择不同的操作系统。如果各位想成为网络编程专家,就必须具备跨平台编程能力。

 

理解网络编程和套接字