1.3 Echo客户程序

Echo是互联网上一个标准的协议,它是一个非常有用的调试和测量工具,Echo服务器简单地把它收到的任何信息发回给客户端。它既可以使用TCP,也可以使用UDP协议,知名端口号是7,下面将基于TCP来实现这个协议。

客户端程序为EchoClnt.c,在文件夹TEchoClnt中。它可以接受两个或三个命令行参数,程序名后面的第一个参数是服务器的地址,如果有第三个参数,则为服务器的端口号,没有时用默认值7,如程序1.1所示。

程序1.1 Echo客户端程序 [EchoClnt.c]

1  #include <stdio.h>
2  #include <winsock2.h>
3  #pragma comment(lib, "ws2_32") /* WinSock使用的库函数  */
4  #define ECHO_DEF_PORT     7 /* 连接的默认端口 */
5  #define ECHO_BUF_SIZE   256 /* 缓冲区的大小*/
6  int main(int argc, char **argv)
7  {
8      WSADATA wsa_data;
9      SOCKET echo_soc = 0;      /* socket句柄 */
10      struct sockaddr_in serv_addr;   /* 服务器地址 */
11      unsigned short port = ECHO_DEF_PORT;
12      int result = 0, send_len = 0;
13      char *test_data = "Hello World!", recv_buf[ECHO_BUF_SIZE];
14      if (argc < 2)
15      {
16           printf("input %s server_address [port]\n", argv[0]);
17           return -1;
18      }
19      if (argc >= 3)
20           port = atoi(argv[2]);
21      WSAStartup(MAKEWORD(2,0), &wsa_data);/* 初始化WinSock资源 */
22      send_len = strlen(test_data);
      /* 服务器地址 */
23      serv_addr.sin_family = AF_INET;
24      serv_addr.sin_port = htons(port);
25      serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
26      if (serv_addr.sin_addr.s_addr == INADDR_NONE)
27      {
28           printf("[ECHO] invalid address\n");
29           return -1;
30      };
31      echo_soc = socket(AF_INET, SOCK_STREAM, 0); /* 创建socket */
32      result = connect(echo_soc, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
33      if (result == 0) /* 连接成功 */
34      {
35           result = send(echo_soc, test_data, send_len, 0);
36           result = recv(echo_soc, recv_buf, ECHO_BUF_SIZE, 0);
37      }
38      if (result > 0)
39      {
40           recv_buf[result] = 0;
41           printf("[Echo Client] receives: \"%s\"\r\n", recv_buf);
42      }
43      else
44           printf("[Echo Client] error : %d.\r\n", WSAGetLastError());
45      closesocket(echo_soc);
46      WSACleanup();
47      return 0;
48  }

头文件

第1~2行,包含了程序需要的头文件,WinSock有两个主要的版本,第一个是版本1.1,使用的头文件是winsock.h,另一个主要的版本是2.2,需要包含的头文件是winsock2.h,版本2.2兼容1.1的实现。

根据你使用的版本,WinSock头文件winsock.h和winsock2.h只需要使用其中之一,它们包含了规范中定义的所有常量、宏、类型、数据结构以及函数原形,在WinSock的头文件中也包含了Windows的标准头文件Windows.h,程序中不需要再包含Windows.h文件。

链接库文件

第3行是一个预编译命令,#pragma是在目标文件或可执行文件中放入一个注释记录,通用格式是#pragma comment(comment_type [, commentstring]),comment_type可以是5个预定义的标识符之一:compiler、exestr、lib、linker、user,其中lib是在目标文件中放入库搜索记录,必须有commentstring参数,包含了想要链接器搜索的库文件。它优先于默认的库搜索记录,链接器搜索这个库文件就像在命令上指定了一样。同一源文件中可以包含多个,它们在目标文件中出现的顺序与源文件的顺序相同。

#pragma comment(lib, "ws2_32"),要求编译器链接ws2_32.lib库文件,等价于在Visual C++ 6.0的Project | Settings | Link | Object/library modules: 中加入ws2_32.lib,如图1.5所示。

图1.5 Visual C++ 6.0 Project Settings

WinSock不同版本使用的头文件与库文件总结,如表1.1所示。

表1.1 WinSock不同版本的头文件和库文件

定义常量

第4~5行,定义程序中要用到的一些常量,这样做使程序比较清晰,易于维护,程序中不会突然出现一些数字,时间长了,程序的作者及读者都不清楚它的意义。另外,就是当这些数字改变时,只需要修改一处,而如果这些数字散布在程序中,就要修改多处,还可能漏掉。

启动函数

第6行,main函数是ANSI C标准定义的程序启动函数,它没有函数原型,返回值是int,但参数有两种形式,一种没有参数:

int main(void) { /*  代码  */ }

另一种带有两个参数,可以是任何名字,但通常命名为argc和argv:

int main(int argc, char *argv[]) { /*  代码  */ }

其中的char *argv[]也可以写成等价形式char **argv。参数argc是非负整数,argv[argc]是NULL,argv[0]是程序的名字,argv[1]到argv[argc-1]是程序的参数。

命令行参数

第14~20行,检查命令行参数,本程序必须至少有2个参数,一个是程序本身的名字,另一个是服务器的地址,如果有第三个参数,则为服务器的端口号,没有则用默认端口号。

WinSock初始化

第21行,WSAStartup初始化WinSock动态链接库,它必须是被应用程序调用的第一个WinSock函数,允许应用程序指定要使用的WinSock版本。

指定地址和端口号

第23~30行,像打电话需要知道对方的电话号码一样,客户端在与服务器建立连接时,也需要知道服务器的一些信息,在socket中是服务器的地址和端口号。socket把这些信息统一放到一个数据结构struct sockaddr_in中,WinSock地址结构如程序1.2所示。

程序1.2 WinSock地址结构

struct sockaddr_in {
    short   sin_family;
    u_short sin_port;
    struct  in_addr sin_addr;
    char    sin_zero[8];
};
struct in_addr {
union {
      struct {u_char s_b1,s_b2,s_b3,s_b4;} S_un_b;
      struct { u_short s_w1,s_w2; } S_un_w;
      u_long S_addr;
} S_un;
#define s_addr  S_un.S_addr
#define s_host  S_un.S_un_b.s_b2
#define s_net   S_un.S_un_b.s_b1
#define s_imp   S_un.S_un_w.s_w2
#define s_impno S_un.S_un_b.s_b4
#define s_lh    S_un.S_un_b.s_b3
};

这个地址结构有四个成员需要程序员填写。

sin_family,地址簇,内核用sin_family来判断怎样解释所包含的地址,Internet地址簇都用AF_INET标识。

sin_port,16位的端口号,必须是网络字节序。

sin_addr,32位的IP地址,在这个程序中是指服务器的地址。值得注意的是sin_addr不是32位的无符号整数,而是一个struct in_addr结构,在WinSock中这是一个联合,如程序1.2所示。用联合带来的便利是允许程序访问IP地址4个字节中的每个字节或者2个16位值中的任一个。在分类IP地址A类、B类和C类中可以容易地获得需要的字节,但随着子网划分及无类域间路由(Classless Inter-Domain Routing,CIDR)地址分配技术的出现,不再区分各种地址类,使用联合也就没有必要了。IP地址也必须是网络字节序。

命令行输入的第二个参数argv[1]是服务器的IP地址,是ASCII格式,第25行中的inet_addr把ASCII格式的地址转换成二进制格式的地址,失败时打印一条出错消息并退出程序。

创建套接字

第31行,创建一个套接口,网络通信的第一步就是创建一个socket描述符并分配相关的资源。它接受三个参数:第一个是AF_INET,指明通信使用的地址簇;第二个是SOCK_STREAM是socket的类型,SOCK_STREAM是流式socket,提供的是面向连接的全双工服务,在发送或接收数据前必须先建立连接;第三个是使用的具体协议,在TCP/IP协议中,只有TCP提供的是数据流服务,所以当类型是SOCK_STREAM时,不用指定具体的协议,参数为0即可。

成功返回的是一个句柄,标识了这个socket,在之后的函数调用中都要使用这个句柄;失败时返回值为INVALID_SOCKET,调用WSAGetLastError可以得到具体的错误码。

建立连接

第32行,函数connect建立与服务器的连接,服务器的地址在第二个参数地址结构中指定,第三个参数是地址结构的长度。第二个参数形式参数的类型与实参是不一致的,形参是一个通用的套接口地址结构指针,传入参数时要做强制类型转换,使用这种形式的主要原因是socket的开发比ANSI C标准早,当时还没有void *通用指针类型可用。

发送和接收

第33~37行,发送数据用send,第二个参数是数据缓冲区,第三个参数告诉TCP/IP协议数据的长度,如果应用程序使用的是TCP协议,服务器地址已经在连接时规定了,因此send中不用指定数据要发送的目的地址,这个函数必须在connect成功后调用,否则会失败,错误码是WSAECONNECT。

接收数据用recv,第二个参数是接收缓冲区,第三个参数是缓冲区的长度,对于字节流类型的socket,它返回尽可能多的不超过缓冲区长度的数据。

显示信息

第38~44行,如果接收到了服务器发过来的数据,显示接收到的数据,失败时在屏幕上输出错误码。

关闭连接

第45~46行,关闭连接,并释放WinSock的资源。

运行结果

测试Echo的客户端程序需要先启动1.3节中的服务器程序,然后在命令行上输入:

>EchoClnt.exe 127.0.0.1

结果如下:

[Echo Client] receives: "Hello World!"