- WinSock网络编程经络
- 张会勇
- 2511字
- 2020-08-28 19:07:24
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!"