Visual C++ 2017网络编程实战
上QQ阅读APP看书,第一时间看更新

5.5 简单的TCP套接字编程

当使用函数socket和WSASocket函数创建的套接字时,默认都是阻塞模式的。阻塞模式是指套接字在执行操作时,调用函数在没有完成操作之前不会立即返回的工作模式。这意味着当调用Winsock API不能立即完成时,线程处于等待状态,直到操作完成。常见的阻塞情况如下:

(1)接受连接函数

函数accept/WSAAcept从请求连接队列中接受一个客户端连接。如果以阻塞套接字为参数调用这些函数,那么当请求队列为空时函数就会阻塞,线程将进入睡眠状态。

(2)发送函数

函数send/WSASend、sendto/WSASendto都是发送数据的函数。当用阻塞套接字作为参数调用这些函数时,如果套接字缓冲区没有可用空间,函数就会阻塞,线程就会睡眠,直到缓冲区有空间。

(3)接收函数

函数recv/WSARecv、recvfrom/WSARecvfrom用来接收数据。当用阻塞套接字为参数调用这些函数时,如果套接字缓冲区没有数据可读,函数就会阻塞,调用线程在数据到来前将处于睡眠状态。

(4)连接函数

函数connect/WSAConnect用于向对方发出连接请求。客户端以阻塞套接字为参数调用这些函数向服务器发出连接时,直到收到服务器的应答或超时才会返回。

使用阻塞模式的套接字开发网络程序比较简单,容易实现。在希望能够立即发送和接收数据且处理的套接字数量较少的情况下,使用阻塞套接字模式来开发网络程序比较合适。它的不足之处表现为:在大量建立好的套接字线程之间进行通信时比较困难。当希望同时处理大量套接字时将无从下手,扩展性差。

【例5.3】一个简单的服务器客户机通信程序

(1)新建一个控制台程序,工程名是test,我们把test工程作为服务器程序。

(2)打开test.cpp,在其中输入如下代码:

        #include "stdafx.h"
        #define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
        #include <Winsock2.h>
        #pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

        int _tmain(int argc, _TCHAR* argv[])
        {
        WORD wVersionRequested;
        WSADATA wsaData;
        int err;

        wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
        err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
        if (err != 0) return 0;

        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) //判断返
    回的版本号是否正确
        {
             WSACleanup();
             return 0;
        }
        //创建一个套接字,用于监听客户端的连接
        SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

        SOCKADDR_IN addrSrv;
        addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); //使用当前主机任意可用IP
        //人为指定一个可用的IP地址
        // addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.2");
        addrSrv.sin_family = AF_INET;
        addrSrv.sin_port = htons(8000);  //使用端口8000

        bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //绑定
        listen(sockSrv, 5); //监听

        SOCKADDR_IN addrClient;
        int len = sizeof(SOCKADDR);

        while (1)
        {
             printf("--------等待客户端-----------\n");
             //从连接请求队列中取出排在最前的一个客户端请求,如果队列为空就阻塞
             SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrClient, &len);
             char sendBuf[100];
             sprintf_s(sendBuf, "欢迎登录服务器(%s)",
inet_ntoa(addrClient.sin_addr));//组成字符串
             send(sockConn, sendBuf, strlen(sendBuf) + 1, 0); //发送字符串给客户端
             char recvBuf[100];
             recv(sockConn, recvBuf, 100, 0); //接收客户端信息
             printf("收到客户端的信息:%s\n", recvBuf); //打印收到的客户端信息
             closesocket(sockConn); //关闭和客户端通信的套接字
        puts("是否继续监听?(y/n)");
             char ch[2];
        scanf_s("%s", ch, 2); //读控制台两个字符,包括回车符
             if (ch[0] != 'y') //如果不是y就退出循环
                  break;
        }
        closesocket(sockSrv); //关闭监听套接字
        WSACleanup(); //释放套接字库
        return 0;
        }

程序很简单。先新建一个监听套接字,然后等待客户端的连接请求,阻塞在accept函数处。一旦有客户端连接请求来了,就返回一个新的套接字,这个套接字和客户端进行通信,通信完毕关掉这个套接字。监听套接字根据用户输入继续监听或退出。在上面的代码中,我们让系统自己选择一个可用的IP地址绑定到套接字上,即“addrSrv.sin_addr.S_un.S_addr =htonl(INADDR_ANY);”,如果要人为指定主机的一个可用IP地址,可以这样:

    addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.2");

(3)在test解决方案中添加一个新建的控制台工程,工程名为client。然后打开client.cpp,在其中输入如下代码:

    #include "stdafx.h"
    #define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
    #include <Winsock2.h>
    #pragma comment(lib, "ws2_32.lib")

    int _tmain(int argc, _TCHAR* argv[])
    {
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2); //初始化Winsock库

    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) return 0;

    //判断返回的版本号是否正确
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
    {
         WSACleanup();
         return 0;
    }
    SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);//新建一个套接字

    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //服务器的IP
    addrSrv.sin_family = AF_INET;
    addrSrv.sin_port = htons(8000); //服务器的监听端口
    //向服务器发出连接请求
    err = connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
    if (SOCKET_ERROR == err) //判断连接是否成功
    {
         printf("连接服务器失败,请检查服务器是否启动\n");
         return 0;
    }
    char recvBuf[100];
    recv(sockClient, recvBuf, 100, 0); //接收来自服务器的信息
    printf("收到来自服务器端的信息:%s\n", recvBuf); //打印收到的信息
    send(sockClient, "你好,服务器", strlen("你好,服务器") + 1,0);//向服务器发送信息

    closesocket(sockClient); //关闭套接字
    WSACleanup(); //释放套接字库

    return 0;
    }

(4)保存工程并运行。运行时先启动服务器程序,再启动客户端程序(可以设为启动项目后再运行)。运行结果如图5-1和图5-2所示。

图5-1

图5-2

【例5.4】统计套接字的connect超时时间

(1)打开VC2017,新建一个控制台工程test。

(2)在test.cpp中输入如下代码:

        #include "stdafx.h"
        #define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
        #include <Winsock2.h>
        #pragma comment(lib, "ws2_32.lib") //Winsock库的引入库
        #include <assert.h>
        #include <stdio.h>
        #include <string.h>
        #include <errno.h>
        #include <stdlib.h>
        #include <fcntl.h>
        #include <time.h>

        #define BUFFER_SIZE 512
        int main(int argc, char* argv[])
        {

        char ip[] = "120.4.6.99"; //120.4.6.99是和本机同一网段的地址,但并不存在
        int port = 13334;
        struct sockaddr_in server_address;

        WORD wVersionRequested;
        WSADATA wsaData;
        int err;

        wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
        err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
        if (err != 0) return 0;

        //判断返回的版本号是否正确
        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
        {
             WSACleanup();
             return 0;
        }

        memset(&server_address,0, sizeof(server_address));
        server_address.sin_family = AF_INET;
        DWORD dwIP = inet_addr(ip);
        server_address.sin_addr.s_addr = dwIP;
        server_address.sin_port = htons(port);

        int sock = socket(PF_INET, SOCK_STREAM, 0);
        assert(sock >= 0);

        long t1 = GetTickCount();

        int ret = connect(sock, (struct sockaddr*)&server_address,
sizeof(server_address));
        printf("connect ret code is: %d\n", ret);
            if (ret == -1)
        {
             long t2 = GetTickCount();
         printf("time used:%dms\n", t2-t1);

         printf("connect failed...\n");
         if (errno == EINPROGRESS)
         {
              printf("unblock mode ret code...\n");
         }
    }
    else
    {
         printf("ret code is: %d\n", ret);
    }
    closesocket(sock);
    WSACleanup(); //释放套接字库
    return 0;
    }

在代码中,首先定义了和本机IP同一子网的不真实存在的IP(120.4.6.99)。如果不是同一子网,connect能很快判断出这个IP不存在,所以超时时间较短。如果是同一子网的假IP,则要等网关回复结果后connect才知道是否能连通。如果将我们的电脑连上Internet,再用一个公网上的假IP,那么超时时间更长,因为要等很多网关、路由器等信息回复后connect才能知道是否可以连上。不过,现在我们同一子网里的假IP用做测试就够了。

(3)保存并运行,运行结果如图5-3所示。

图5-3