• WinSock异步编程


    WinSock异步编程

    简介
    • 同步

      每个函数在下一条语句执行以前必须完成

    • 异步

      Windows的消息是异步的,不按照事先定义的顺序发生的。

      当程序开始一个任务时,可以告诉Windows在任务完成时发送一条消息,收到消息时根据任务的完成结果再决定下一步做什么,处理完这条消息,控制权又返回给Windows,系统继续执行其他的任务。

    同步模型中,当执行一些需要花费很长时间才能完成的功能时,程序会被阻塞,无法进行其他的操作,只能等待这个功能完成。

    异步WinSock则不同,在执行一个费时的网络操作时,程序用WSAAsyncSelect向Windows系统注册一条消息,指明感兴趣的网络事件,告诉系统任务完成时用这条消息来通知程序。这样,正在进行的网络操作如果不能立即完成,会返回错误码,告诉系统正在处理而不会阻塞程序,程序还可以进行其他的各种操作。网络操作完成时,无论成功还是失败,应用程序的窗口过程会收到之前注册的消息,消息中有操作完成的结果,程序根据消息中的参数判断发生了什么网络事件,决定下一步的工作。

    WSAAsyncSelect

    该函数提供一个异步I/O模型,使用它应用程序不会在调用某一个套接口函数时阻塞,函数会立即返回给调用者。

    当要求的操作完成时,应用程序会收到消息,程序根据消息中指明的事件来决定做什么样的处理。

    image-20221104204030719

    int WSAAPI WSAAsyncSelect(SOCKET s, HWND hWnd,u_int wMsg,long IEvent);
    
    • 1

    成功返回0,表明应用程序的事件注册成功;

    失败返回SOCKET_ERROR,应用程序可以调用WSAGetLastError()得到具体的错误码。

    • s 要求事件通知的套接口描述符
    • hWnd 窗口句柄,网络事件发生时向这个窗口发消息
    • wMsg 网络事件发生时应用程序会收到的消息
    • IEvent 事件组合

    WSAAsyncSelect要求WinSock监测套接口s上的事件,当检查到lEvent参数中规定的网络事件时,向窗口hWnd发送wMsg消息。

    会自动把套接口设置为非阻塞模式,要再设为阻塞模式,应用程序必须再调用WSAAsyncSelect ,并且将IEvent 参数设置为0,清除与套接口相关的事件,然后调用ioctlsocket 设置为阻塞模式

    程序感兴趣的事件必须一次设置完成, 后面的设置会覆盖前面的

    套接口网络事件

    事件说明
    FD_ACCEPT接收进入的连接请求通知
    FD_CLOSE套接口关闭通知
    FD_CONNECT连接建立完成通知
    FD_READ套接口缓冲区收到数据,可以读取数据通知
    FD_WRITE套接口缓冲区有空间发送应用程序的数据,可以发送数据通知

    当套接口上指定的发生网络事件的应用程序收到wMsg时,wParam是发生网络事件的套接口句柄,IParam 的低16字节表明发生了什么事件,高16位是错误码

    WinSock定义了两个宏来提取网络事件和错误码

    #define WSAGETSELECTEVENT(lParam) LOWORD(lParam)
    #define WSAGETSELECTERROR(lParam) HIWORD(lParam)
    
    • 1
    • 2

    调用WSAAsyncSelect的返回值与网络事件错误之间的区别

    • 调用WSAAsyncSelect函数失败时,可以用WSAGetLastError()得到具体的错误码
    • 收到网络事件通知时,只能用WSAGETSELECTERROR提取错误码,不能用WSAGetLastError函数
    • 调用函数错误与网络事件错误是不同的,函数错误是指函数是否已经成功启动了用户要求的操作,而网络事件是指事件是否有错误。
    • 如果函数调用失败,一定不会收到网络事件;成功时,收到的网络事件可能是成功,也可能是失败。

    套接口上的事件具有继承性,套接口上的事件具有继承性,调用accept接受新的连接创建的套接口与侦听套接口有同样的事件属性。

    如果侦听套接口上调用WSAAsyncSelect设置了FD_ACCEPT、FD_READ、FD_WRITE和FD_CLOSE,那么在侦听套接口上接受的任何套接口上都有这些事件,并且在与侦听套接口相同的消息上接收这些网络事件通知。

    如果应用程序想在accept的套接口上有不同的消息和事件,需要重新调用WSAAsyncSelect设置事件通知条件

    WinSock向应用程序的窗口发送了一个网络事件后,不会再发送同样的事件通知,除非应用程序调用了相关函数,重新启用了事件通知。如:服务器连续调用两次send向客户端发送数据,第一次200字节,会收到FD_READ通知,如果客户端没有调用recv接收这200字节数据,那么后面再发送100字节数据到达客户端时,就不会再有FD_READ通知。直到客户端调用recv后,才会重新启用FD_READ事件

    套接口事件及通知条件

    事件重新启用函数通知条件
    FD_ACCEPTaccept1. 只适用于面向连接套接口 2. 调用WSAAsyncSelect时,队列中有连接请求 3. 接收到新的连接请求,并且还没有发送FD_ACCEPT 4. 调用accept后,队列中仍然有连接请求
    FD_CLOSE1. 只适用于面向连接的套接口,调用closesocket后不会再收到FD_CLOSE 2. 调用WSAAsyncSelect时,连接已经被关闭 3. 对方关闭了连接(发送FIN 或者 RST)
    FD_CONNECT1. 调用WSAAsyncSelect时,已经建立了连接 2. 调用connect函数后,返回WSAEWOULDBLOCK, 在连接完成时,会收到FD_CONNECT
    FD_READrecv,recvfrom1. 调用WSAAsyncSelect时,接收缓冲区中有数据 2. 接收到新的数据,且没有发送FD_READ 3. 调用recv,recvfrom后缓冲区内仍然有数据或者设置SO_OOBINLINE 为TRUE, 而有OOB数据 ( OOB带外数据 )
    FD_WRITEsend,sendto1. 调用WSAAsyncSelect时,send或sendto 可以成功 2. 调用connect或者accept后,连接建立成功 3. 调用send或者sendto失败,错误码为WSAEWOULDBLOCK,而当发送缓冲区又有空间时 4. 无连接套接口调用bind成功后,总是可写的
    FD_OOB1. 调用setsockopt设置SO_OOBINLINE为FALSE,条件满足时才会收到FD_OOB 2. 调用WSAAsyncSelect时,接收缓冲区中有OOB数据 3. 收到OOB数据,并且还没有发送FD_OOB 4. 调用recv或者 recvfrom后,缓冲区中还有OOB数据
    • FD_ACCEPT

      只有listen套接口会收到FD_ACCEPT事件。

      当有新的客户与服务器建立连接时,服务器应用程序会收到FD_ACCEPT事件。

      收到FD_ACCEPT事件后,要调用accept函数接收新的连接

    • FD_CONNECT

      客户端的应用程序才会收到FD_CONNECT。

      应用程序调用connect函数与服务器建立连接,连接过程经过三次握手,建立连接不会立即成功。

      无论成功与否,connect 会先返回给调用程序,错误码为WSAEWOULDBLOCK,连接完成后客户端应用程序会收到FD_CONNECT事件通知。

    • FD_READ

      当有新的数据到达,并且还没有发送FD_READ时,会向应用程序发送FD_READ事件通知。

      收到FD_READ应用程序调用recv接收数据,应用程序不需要一次读完所有的数据。

      在Windows中是事件驱动的,当调用recv时,如果数据没有读完,Windows还会发送FD_READ通知。

      对于一个FD_READ事件,应用程序如果调用了多次recv,每次都没读完数据,那么每个recv都会导致系统发送一个FD_READ通知。

      为了避免这种情况,应用程序可以在recv前用WSAAsyncSelect取消FD_READ事件。

      收到FD_READ通知不一定就能接收到数据,通知有可能是前面recv时导致发送的,但数据已接收完。

      WinSock不合理之处:当调用recv恰好接收完数据,即使缓冲区中已经没有数据了,还是会发送FD_READ

    • FD_WRITE

      收到FD_WRITE意味着套接口是可写的,可以调用send或sendto发送数据。

      的,可以调用send或sendto发送数据。当第一次调用connect连接成功或调用accept接受一个连接时,应用程序都会收到FD_WRITE。

      连接建立成功后,就可以发送数据,如果应用程序发送的数据比较多,TCP/IP协议不能及时把数据发送给对方,应用程序的数据会暂时保存在套接口发送缓冲区中。

      当缓冲区满时,再调用send就会失败,错误为WSAEWOULDBLOCK。

      协议把数据发送出去,发送缓冲区又有空间时,会向应用程序发送FD_WRITE通知。

    • FD_OOB

      当套接口被配置为单独接收紧急数据,即紧急数据不与正常数据一起接收,并收到了对方send(MSG_OOB)发送的紧急数据时,应用程序会收到FD_OOB事件通知。

      收到FD_OOB事件通知,应用程序需要调用recv(MSG_OOB)接收紧急数据。

      • 接收缓冲区中只有OOB数据,调用recv,标志为0,则会失败,错误码为WSAEWOULDBLOCK。调用recv(MSG_OOB)会收到OOB数据。

      • 发送OOB数据send(soc,data,len,MSG_OOB),无论len是多长,接收到的OOB只有一个字节,是data的最后一个字节。

        当len大于1时,接收数据的应用程序会先收到FD_OOB事件,调用recv(MSG_OOB)接收data最后一个字节的OOB数据。之后会收到FD_READ,调用recv接收OOB前面的正常数据

        image-20221105155401416

      • 发送n次OOB时,只要send之间的间隔较长,数据没有在发送时组合到一个分组中,而接收方一直没有接收OOB,最后一起接收时,会一次收到n个字节的OOB。

        WinSock把OOB放在了一个单独的接收缓冲区中

      • 是否单独接收紧急数据是用setsockopt(SO_OOBINLINE)设置的,默认是单独接收OOB

        调用setsockopt(SO_OOBINLINE),选项值为TRUE时,表示把OOB当作正常数据接收,这样即使收到了OOB,应用程序也不会收到FD_OOB事件,而是FD_READ通知。

      • FD_CLOSE

        只适用于面向连接套接口,对方调用shutdown或closesocket关闭连接时会收到FD_CLOSE通知。

        建议收到FD_CLOSE,先调用recv把数据接收完再关闭套接口

        FD_CLOSE消息的错误码 --> 表示对方是否是正常关闭还是放弃连接

        • 0 正常关闭
        • WSAECONNRESET 连接被重设,对方放弃连接
    Finger协议

    Finger协议的主要功能是查询某一主机上的用户信息。

    主机返回用户容易阅读的状态报告,包括用户名、终端位置、任务名称、空闲时间等。

    Finger协议的知名端口号是79,协议的格式没有特别的要求,大部分情况下客户端只需要发送一个"命令行"。根据"命令行"和服务器的不同,客户端收到的信息会有所变化。

    服务器一发送完数据就关闭连接。

    • 如果客户端发送的是空行,即只有,服务器把当前使用系统的所有用户都发送给客户端。

    • 如果客户端规定了用户名,如"Alice",那么服务器应该只把这个用户的情况报告给客户端。

    • 如果用户名与服务器上的多个登录用户匹配,这些匹配的用户信息应该都发送给客户端。

    • 当查询的用户没有登录时,服务器报告用户名、最后的注销时间。

      用户也可以留下一条短消息,服务器在应答中包含这条消息。

    常用的操作系统,如Linux、Windows都带有Finger程序,基本格式为:finger [user]@host。其中user规定了想要查询的用户,它是可选的,没有这个参数,则显示服务器上所有用户的信息。而host指定了要查询的服务器。

    Finger服务器程序
    1. 服务器程序固定在知名端口79上侦听
    2. 预先定义用户信息 --> 用于测试
    3. 目前程序只有开始菜单,没有结束或者停止菜单
    4. 开始Finger服务器,需要单击File菜单中的Start

    FingerSrv.c

    #define _WINSOCK_DEPRECATED_NO_WARNINGS
    #define _CRT_SECURE_NO_WARNINGS
    #include 
    #include 
    #include 
    #include "resource1.h"
    #pragma comment(lib, "ws2_32.lib") /* WinSock使用的库函数 */
    #define WM_SOCKET_NOTIFY   (WM_USER + 11) /* 自定义socket消息 */
    #define FINGER_DEF_PORT    79 /* 侦听的端口 */
    #define ID_EDIT_LOG        1  /* 日志控件的标识 */
    /* 定义控件的风格 */
    #define EDIT_STYLE  (WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | \
                            ES_MULTILINE | WS_HSCROLL | WS_VSCROLL)
    #define FINGER_LINE_END  "\r\n" /* 行结束符 */
    #define FINGER_MAX_BUF    1024  /* 最大的接收缓冲区 */
    #define FINGER_MAX_SOC       8  /* 最多可以接受的客户数 */
    #define TABLE_SIZE(a) (sizeof(a) / sizeof(a[0]))
    /* 定义用户信息 */
    struct UserInfo
    {
        const char* szUser;  // 用户名 
        const char* szFullName; // 全称 
        const char* szMessage;  // 留下的消息 
    };
    struct UserInfo FingerUser[] =
    {
        { "Alice",  "Alice Joe",   "Welcome! I am on vacation." },
        { "Smith",  "Smith David", "Learn to fly like a bird." },
        { "Rubin",  "Jeff Rubin",  "How are you!" }
    };
    /* 定义全局变量 */
    static HWND hWndLog;       /* 输出日志信息的窗口句柄 */
    static SOCKET hLstnSoc;    /* 侦听socket句柄 */
    static SOCKET hClntSoc[FINGER_MAX_SOC]; /* 客户端的socket句柄 */
    static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);  // 窗口处理函数 
    static SOCKET FingerListenSoc(HWND hWnd, unsigned short port);
    static void FingerOnSocketNotify(WPARAM wParam, LPARAM lParam);
    static void LogPrintf(const TCHAR* szFormat, ...);
    
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
        PSTR szCmdLine, int iCmdShow)
    {
        TCHAR szClassName[] = TEXT("FingerSrv");
        HWND        hWnd;
        MSG         msg;
        WNDCLASS    wndclass;
        WSADATA     wsaData;
        int i;
        WSAStartup(WINSOCK_VERSION, &wsaData); /* 初始化 */
        memset(hClntSoc, INVALID_SOCKET, FINGER_MAX_SOC * sizeof(SOCKET));
    
        /* 注册窗口类 */
        wndclass.style = CS_HREDRAW | CS_VREDRAW;
        wndclass.lpfnWndProc = WndProc;  /* 窗口过程处理函数 */
        wndclass.cbClsExtra = 0;
        wndclass.cbWndExtra = 0;
        wndclass.hInstance = hInstance;
        wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
        wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wndclass.lpszMenuName = (LPCSTR)IDR_FINGER_SRV; /* 菜单 */
        wndclass.lpszClassName = szClassName; /* 窗口类名 */
        if (!RegisterClass(&wndclass))
        {
            MessageBox(NULL, TEXT("TRequires Windows NT!"), szClassName, 0);
            return 0;
        }
    
        // 在内存中创建窗口 
        hWnd = CreateWindow(szClassName,          /* 与注册的类名相同 */
            TEXT("Finger Server"),/* 窗口标题 */
            WS_OVERLAPPEDWINDOW,  /* 窗口风格 */
            CW_USEDEFAULT,        /* 初始x坐标 */
            CW_USEDEFAULT,        /* 初始y坐标 */
            CW_USEDEFAULT,        /* 初始宽度 */
            CW_USEDEFAULT,        /* 初始高度 */
            NULL,                 /* 父窗口句柄 */
            NULL,                 /* 菜单句柄 */
            hInstance,            /* 程序实例句柄 */
            NULL);                /* 程序参数 */
        ShowWindow(hWnd, iCmdShow); /* 显示窗口 */
        UpdateWindow(hWnd);         /* 更新窗口 */
        /* 消息循环 */
        while (GetMessage(&msg, NULL, 0, 0))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        /* 关闭socket */
        for (i = 0; i < FINGER_MAX_SOC; i++)
        {
            if (hClntSoc[i] != INVALID_SOCKET)
                closesocket(hClntSoc[i]);
        }
        closesocket(hLstnSoc);
        WSACleanup();
        return msg.wParam;
    }
    
    
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
        WPARAM wParam, LPARAM lParam)
    {
        int cxClient, cyClient;
        int wmId, wmEvent;
        switch (message)
        {
        case WM_CREATE:
            MessageBox(hWnd, "Start", "infor", 0);
            hWndLog = CreateWindow(TEXT("edit"), NULL, EDIT_STYLE,
                0, 0, 0, 0, hWnd, (HMENU)ID_EDIT_LOG,
                ((LPCREATESTRUCT)lParam)->hInstance, NULL);
            return 0;
        case WM_SIZE:
            cxClient = LOWORD(lParam);
            cyClient = HIWORD(lParam);
            MoveWindow(hWndLog, 0, 0, cxClient, cyClient, FALSE);
            return 0;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        case WM_COMMAND:
            wmId = LOWORD(wParam);
            wmEvent = HIWORD(wParam);
            switch (wmId)
            {
            case IDM_START:
                MessageBox(hWnd, "IDM_START", "infor", 0);
                hLstnSoc = FingerListenSoc(hWnd, FINGER_DEF_PORT);
                if (hLstnSoc == INVALID_SOCKET)
                    MessageBox(hWnd, TEXT("Listen error"), TEXT("Finger"), 0);
                return 0;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                return 0;
            }
            break;
        case WM_SOCKET_NOTIFY:
            FingerOnSocketNotify(wParam, lParam);
            return 0;
        }
        return DefWindowProc(hWnd, message, wParam, lParam);// 系统默认处理函数 
    }
    
    static SOCKET FingerListenSoc(HWND hWnd, unsigned short port)
    {
        struct sockaddr_in soc_addr; /* socket地址结构 */
        SOCKET lstn_soc; /* 侦听socket句柄 */
        int result;
        /* 创建侦听socket */
        lstn_soc = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        result = WSAAsyncSelect(lstn_soc, hWnd, WM_SOCKET_NOTIFY,
            FD_ACCEPT | FD_READ | FD_CLOSE);
        /* 由系统来分配地址 */
        soc_addr.sin_family = AF_INET;
        soc_addr.sin_port = htons(port);
        soc_addr.sin_addr.s_addr = INADDR_ANY;
        /* 绑定socket */
        result = bind(lstn_soc, (struct sockaddr*)&soc_addr, sizeof(soc_addr));
        if (result == SOCKET_ERROR)
        {
            closesocket(lstn_soc);
            return INVALID_SOCKET;
        }
        //套接口所对应的TCP控制块从CLOSED状态转变到LISTEN状态
        result = listen(lstn_soc, SOMAXCONN); /* 侦听来自客户端的连接 */
        if (result == SOCKET_ERROR)
        {
            closesocket(lstn_soc);
            return INVALID_SOCKET;
        }
        LogPrintf(TEXT("Finger server is running ...\r\n"));
        return lstn_soc;
    }
    
    static SOCKET FingerOnAccept(SOCKET lstn_soc)
    {
        struct sockaddr_in soc_addr; /* socket地址结构 */
        int i, addr_len = sizeof(soc_addr); /* 地址长度 */
        SOCKET data_soc;
        /* 接受新的连接 */
        data_soc = accept(lstn_soc, (struct sockaddr*)&soc_addr, &addr_len);
        if (data_soc == INVALID_SOCKET)
        {
            LogPrintf(TEXT("accept error: %i.\r\n"), WSAGetLastError());
            return INVALID_SOCKET;
        }
        for (i = 0; i < FINGER_MAX_SOC; i++)
        {
            if (hClntSoc[i] == INVALID_SOCKET)
            {
                hClntSoc[i] = data_soc;
                break;
            }
        }
        return data_soc;
    }
    
    static int FingerOnRead(SOCKET clnt_soc)
    {
        int i, j, result, buflen = FINGER_MAX_BUF - 1;
        int iFind = 0, iCount = TABLE_SIZE(FingerUser);
        char cBuf[FINGER_MAX_BUF], cSendBuf[FINGER_MAX_BUF], * pEnd;
        struct UserInfo* pUser;
        /* 查找客户端对应的socket句柄 */
        for (i = 0; i < FINGER_MAX_SOC; i++)
        {
            if (hClntSoc[i] == clnt_soc)
                break;
        }
        if (i == FINGER_MAX_SOC)
            return FALSE;
        result = recv(clnt_soc, cBuf, buflen, 0);  /* 接收数据 */
        if (result <= 0)
        {
            closesocket(clnt_soc);
            hClntSoc[i] = INVALID_SOCKET;
            return FALSE;
        }
        cBuf[result] = 0; // 在后方添加一个'\0', 也是buflen = FINGER_MAX_BUF -1的原因 
        LogPrintf(TEXT("recv >: %s\r\n"), cBuf);
        /* 搜索用户名结尾的 "\r\n" */
        pEnd = strstr(cBuf, FINGER_LINE_END);
        if ((pEnd != NULL) && (pEnd != cBuf))
            *pEnd = 0; /*结尾的 "\r\n" 换成 0, 便于查找 */
        for (j = 0; j < iCount; j++) /* 查找用户信息 */
        {
            pUser = &FingerUser[j];
            if (strcmp(cBuf, FINGER_LINE_END) == 0) /* 所有用户 */
                buflen = sprintf(cSendBuf, "%s\r\n", pUser->szUser);
            else if (strcmp(cBuf, pUser->szUser) == 0) /* 特定用户 */
                buflen = sprintf(cSendBuf, "%s: %s, %s\r\n", pUser->szUser,
                    pUser->szFullName, pUser->szMessage);
            else
                continue;
            iFind++;
            result = send(clnt_soc, cSendBuf, buflen, 0);
            if (result > 0)
                LogPrintf(TEXT("send <: %s\r\n"), cSendBuf);
        }
        if (!iFind)
            send(clnt_soc, "The user is not found.\r\n", 24, 0);
        closesocket(clnt_soc); // 关闭连接 
        hClntSoc[i] = INVALID_SOCKET;
        return TRUE;
    }
    
    static void FingerOnClose(SOCKET clnt_soc)
    {
        int i;
        /* 查找客户端对应的socket句柄 */
        for (i = 0; i < FINGER_MAX_SOC; i++)
        {
            if (hClntSoc[i] == clnt_soc)
            {
                closesocket(clnt_soc);
                hClntSoc[i] = INVALID_SOCKET;
                break;
            }
        }
    }
    
    static void FingerOnSocketNotify(WPARAM wParam, LPARAM lParam)
    {
        int iResult = 0;
        WORD wEvent, wError;
        wEvent = WSAGETSELECTEVENT(lParam); /* LOWORD */
        wError = WSAGETSELECTERROR(lParam); /*HIWORD */
        switch (wEvent)
        {
        case FD_READ:
            if (wError)
            {
                LogPrintf(TEXT("FD_READ error #%i."), wError);
                return;
            }
            FingerOnRead(wParam);
            break;
        case FD_ACCEPT:
            if (wError || (wParam != hLstnSoc))
            {
                LogPrintf(TEXT("FD_ACCEPT error #%i."), wError);
                return;
            }
            FingerOnAccept(wParam);
            break;
        case FD_CLOSE:
            FingerOnClose(wParam);
            break;
        }
    }
    
    static void LogPrintf(const TCHAR * szFormat, ...)
    {
        int iBufLen = 0, iIndex;
        TCHAR szBuffer[FINGER_MAX_BUF];
        va_list pVaList;
        va_start(pVaList, szFormat);
    #ifdef UNICODE
        iBufLen = _vsnwprintf(szBuffer, FINGER_MAX_BUF, szFormat, pVaList);
    #else
        iBufLen = _vsnprintf(szBuffer, FINGER_MAX_BUF, szFormat, pVaList);
    #endif
        va_end(pVaList);
        iIndex = GetWindowTextLength(hWndLog);
        SendMessage(hWndLog, EM_SETSEL, (WPARAM)iIndex, (LPARAM)iIndex);
        SendMessage(hWndLog, EM_REPLACESEL, FALSE, (LPARAM)szBuffer);
        SendMessage(hWndLog, EM_SCROLLCARET, 0, 0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310

    resource1.h

    //{{NO_DEPENDENCIES}}
    // Microsoft Visual C++ 生成的包含文件。
    // 供 Resource.rc 使用
    //
    #define IDR_MENU1                       101
    #define IDR_FINGER_SRV                  101
    #define ID_CMD_ST                       40001
    #define ID_CMD_                         40002
    #define ID_CMD_EXIT                     40003
    #define IDM_START                       40004
    #define IDM_EXIT                        40005
    
    // Next default values for new objects
    // 
    #ifdef APSTUDIO_INVOKED
    #ifndef APSTUDIO_READONLY_SYMBOLS
    #define _APS_NEXT_RESOURCE_VALUE        102
    #define _APS_NEXT_COMMAND_VALUE         40006
    #define _APS_NEXT_CONTROL_VALUE         1001
    #define _APS_NEXT_SYMED_VALUE           101
    #endif
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    Resource.rc

    // Microsoft Visual C++ generated resource script.
    //
    #include "resource1.h"
    #define APSTUDIO_READONLY_SYMBOLS
    /
    //
    // Generated from the TEXTINCLUDE 2 resource.
    //
    #include "winres.h"
    /
    #undef APSTUDIO_READONLY_SYMBOLS
    /
    // 中文(简体,中国) resources
    #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS)
    LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
    #pragma code_page(936)
    #ifdef APSTUDIO_INVOKED
    /
    //
    // TEXTINCLUDE
    //
    1 TEXTINCLUDE 
    BEGIN
        "resource1.h\0"
    END 
    2 TEXTINCLUDE 
    BEGIN
        "#include ""winres.h""\r\n"
        "\0"
    END
    3 TEXTINCLUDE 
    BEGIN
        "\r\n"
        "\0"
    END
    #endif    // APSTUDIO_INVOKED
    /
    //
    // Menu
    //
    IDR_FINGER_SRV MENU
    BEGIN
        POPUP "CMD"
        BEGIN
            MENUITEM "Start",                       IDM_START
            MENUITEM SEPARATOR
            MENUITEM "Exit",                        IDM_EXIT
        END
    END
    
    #endif    // 中文(简体,中国) resources
    /
    #ifndef APSTUDIO_INVOKED
    /
    //
    // Generated from the TEXTINCLUDE 3 resource.
    //
    /
    #endif    // not APSTUDIO_INVOKED
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    image-20221106094724068

    Finger客户端程序

    Finger客户端程序FingerClnt根据用户的输入向服务器发送请求,查询对应用户的信息。

    程序可以输入的信息有两个:用户名和主机。

    • 用户名是要向服务器查询的用户名称;

      用户名可以为空,为空时,客户端只向服务器发送一个空行“\r\n”,要求得到服务器所有已登录用户的信息。

      用户名不为空时,程序取得用户名,在后面追加协议要求的行结束符“\r\n”,调用send把数据发送给服务器,查询这个特定用户的信息。

    • 主机是服务器的IP地址或域名。

      使用协议规定的知名端口。

    FingerClnt.c

    #include 
    #include 
    #include 
    #pragma comment(lib, "ws2_32.lib")  /* WinSock使用的库函数 */
    #define WM_GETHOST_NOTIFY  (WM_USER + 1)  /* 定义域名查询消息 */
    #define WM_SOCKET_NOTIFY   (WM_USER + 11) /* 定义socket消息 */
    #define FINGER_DEF_PORT     79 /* 侦听的端口 */
    #define FINGER_NAME_LEN    256 /* 一般名字缓冲区长度 */
    #define FINGER_MAX_BUF    1024 /* 最大的接收缓冲区 */
    /* 定义控件的风格 */
    #define STATIC_STYLE   (WS_CHILDWINDOW | WS_VISIBLE | SS_LEFT)
    #define BUTTON_STYLE   (WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON)
    #define EDIT_STYLE     (WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT)
    #define EDIT_STYLE_EXT (EDIT_STYLE | ES_MULTILINE | ES_READONLY | \
                            WS_HSCROLL | WS_VSCROLL)
    /* 控件的标识, 是控件在数组中的下标 */
    #define ID_EDIT_USER    1  /* 用户 */
    #define ID_EDIT_HOST    3  /* 主机 */
    #define ID_BTN_FINGER   4  /* 查询按钮的ID */
    #define ID_EDIT_LOG     5  /* 日志控件的标识 */
    #define TABLE_SIZE(a) (sizeof(a) / sizeof( a[0]) )
    /* 控件的属性结构 */
    struct Widget
    {
        int iLeft;      /* 左上角的x坐标 */
        int iTop;       /* 左上角的y坐标 */
        int iWidth;     /* 宽度 */
        int iHeigh;     /* 高度 */
        int iStyle;     /* 控件的风格 */
        TCHAR *szType;  /* 控件类型: button, edit etc. */
        TCHAR *szTitle; /* 控件上显示的文字 */
    };
    struct Finger
    {
        HWND   hWnd;        /* 窗口句柄 */
        HANDLE hAsyncHost;  /* 域名查询句柄 */
        SOCKET hSoc;        /* socket句柄  */
        char cHostEnt[MAXGETHOSTSTRUCT]; /* 域名查询缓冲区 */
        char szUser[FINGER_NAME_LEN];    /* 用户名 */
        char szHost[FINGER_NAME_LEN];    /* 主机 */
    };
    /* 定义Finger程序使用的控件 */
     static struct Widget FgrWgt[] =
    {
        /* 用户名 */
        { 1, 1, 6,  2, STATIC_STYLE, TEXT("static"), TEXT("用户:") },
        { 7, 1, 24, 2, EDIT_STYLE,   TEXT("edit"), TEXT("Alice") },
        /* 主机 */
        { 33, 1, 6,  2, STATIC_STYLE, TEXT("static"), TEXT("主机:") },
        { 38, 1, 24, 2, EDIT_STYLE,   TEXT("edit"), TEXT("127.0.0.1") },
        /* Finger按钮 */
        { 64, 1, 12, 2, BUTTON_STYLE, TEXT("button"), TEXT("Finger") },
        /* 信息 */
        { 1, 4, 64, 20, EDIT_STYLE_EXT, TEXT("edit"), TEXT("") }
    };
    /* 定义全局变量 */
    static HWND hWndWgt[TABLE_SIZE(FgrWgt)];
    static struct Finger gFingerCtrl = { 0, 0, INVALID_SOCKET };
    static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
    static void LogPrintf(const TCHAR *szFormat, ...);
    
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                          PSTR szCmdLine, int iCmdShow)
    {
        TCHAR szClassName[] = TEXT("FingerClnt");
        MSG         msg;
        WNDCLASS    wndclass;
        WSADATA     wsaData;
        WSAStartup(WINSOCK_VERSION, &wsaData); /* 初始化 */
        /* 注册窗口类 */
        wndclass.style         = CS_HREDRAW | CS_VREDRAW;
        wndclass.lpfnWndProc   = WndProc;  /* 这是窗口过程 */
        wndclass.cbClsExtra    = 0;
        wndclass.cbWndExtra    = 0;
        wndclass.hInstance     = hInstance;
        wndclass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
        wndclass.hCursor       = LoadCursor(NULL, IDC_ARROW);
        wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
        wndclass.lpszMenuName  = NULL; /* 菜单 */
        wndclass.lpszClassName = szClassName; /* 窗口类名 */
        if(!RegisterClass(&wndclass))
        {
            MessageBox(NULL, TEXT("Requires Windows NT!"), szClassName, 0);
            return 0;
        }
        gFingerCtrl.hWnd = CreateWindow(szClassName,/* 与注册类名相同 */
                TEXT("Finger Client"),/* 窗口标题 */
                WS_OVERLAPPEDWINDOW,  /* 窗口风格 */
                CW_USEDEFAULT,        /* 初始x坐标 */
                CW_USEDEFAULT,        /* 初始y坐标 */
            	CW_USEDEFAULT,        /* 初始宽度 */
                CW_USEDEFAULT,        /* 初始高度 */
                NULL,                 /* 父窗口句柄 */
                NULL,                 /* 菜单句柄 */
                hInstance,            /* 程序实例句柄 */
                NULL
    	);                /* 程序参数 */
        ShowWindow(gFingerCtrl.hWnd, iCmdShow); /* 显示窗口 */
        UpdateWindow(gFingerCtrl.hWnd);         /* 更新窗口 */
        /* 消息循环 */
        while(GetMessage(&msg, NULL, 0, 0))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        if (gFingerCtrl.hSoc != INVALID_SOCKET)
            closesocket(gFingerCtrl.hSoc);
        WSACleanup();
        return msg.wParam;
    }
    
    static void FingerOnCreate(HWND hWnd, WPARAM wParam, LPARAM lParam)
    {
        HINSTANCE hInstance = ((LPCREATESTRUCT) lParam)->hInstance;
        int i, iCount = TABLE_SIZE(FgrWgt);
        int cxChar, cyChar;
        struct Widget *pWgt;
        // GetDialogBaseUnits返回系统的对话基本单位
    	// 该基本单位为系统字体字符的平均宽度和高度。
        cxChar = LOWORD(GetDialogBaseUnits());
        cyChar = HIWORD(GetDialogBaseUnits());
        /* 创建控件 */
        for (i = 0; i < iCount; i++)
        {
            pWgt = &FgrWgt[i];
            hWndWgt[i] = CreateWindow(pWgt->szType, pWgt->szTitle,
                   pWgt->iStyle, pWgt->iLeft * cxChar, pWgt->iTop * cyChar,
                   pWgt->iWidth * cxChar, pWgt->iHeigh * cyChar,
                   hWnd, (HMENU) i, hInstance, NULL);
        }
    }
    
    static BOOL FingerOnCommand(HWND hWnd,WPARAM wParam,LPARAM lParam)
    {
        int wmId = LOWORD(wParam), wmEvent = HIWORD(wParam);
        /* 处理BN_CLICKED, 得到用户输入的信息 */
        if ((wmId == ID_BTN_FINGER) && (wmEvent == BN_CLICKED))
        {
            if (gFingerCtrl.hAsyncHost)
                   return TRUE;
            if (gFingerCtrl.hSoc != INVALID_SOCKET)
            {
                closesocket(gFingerCtrl.hSoc);
                gFingerCtrl.hSoc = INVALID_SOCKET;
            }
            GetWindowText(hWndWgt[ID_EDIT_USER], gFingerCtrl.szUser,
                   FINGER_NAME_LEN);
            GetWindowText(hWndWgt[ID_EDIT_HOST], gFingerCtrl.szHost,
                   FINGER_NAME_LEN);
            // 异步方式 
            gFingerCtrl.hAsyncHost = WSAAsyncGetHostByName(hWnd,
                WM_GETHOST_NOTIFY, gFingerCtrl.szHost,
                gFingerCtrl.cHostEnt, MAXGETHOSTSTRUCT);
            if (gFingerCtrl.hAsyncHost == 0)
                MessageBox(hWnd, TEXT("Get Host Error"), NULL, 0);
            return TRUE;
        }
        return FALSE;
    }
    
    static SOCKET FingerQuery(HWND hWnd)
    {
        struct sockaddr_in soc_addr; /* socket地址结构 */
        SOCKET soc; /* Finger的socket句柄 */
        int result;
        unsigned long addr;
        struct hostent *host_ent;
        host_ent = (struct hostent *)gFingerCtrl.cHostEnt;
        addr = *(unsigned long *)host_ent->h_addr; /* 网络字节序 */
        soc = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        result = WSAAsyncSelect(soc, hWnd, WM_SOCKET_NOTIFY,
                       FD_CONNECT | FD_READ | FD_CLOSE);
        /* 连接的地址和端口 */
        soc_addr.sin_family = AF_INET;
        soc_addr.sin_port = htons(FINGER_DEF_PORT);
        soc_addr.sin_addr.s_addr = addr;
        /* 与服务器建立连接 */
        result = connect(soc, (struct sockaddr *)&soc_addr, sizeof(soc_addr));
        // 返回0 表示已经成功建立连接了 
        if ((result == SOCKET_ERROR) &&
               (WSAGetLastError() != WSAEWOULDBLOCK))
        {
            closesocket(soc);
            MessageBox(hWnd, TEXT("Can't connect server"), NULL, 0);
            return INVALID_SOCKET;
        }
        return soc;
    }
    
    static int FingerOnConnect(SOCKET clnt_soc)
    {
        int result;
        char cSendBuf[FINGER_MAX_BUF];
        result = sprintf(cSendBuf, "%s\r\n", gFingerCtrl.szUser);
        result = send(clnt_soc, cSendBuf, result, 0);
        return result;
    }
    
    static int FingerOnRead(SOCKET clnt_soc)
    {
        int result, buflen = FINGER_MAX_BUF -1;
        char cBuf[FINGER_MAX_BUF];
        /* 接收数据 */
        result = recv(clnt_soc, cBuf, buflen, 0);
        if (result <= 0)
        {
            closesocket(clnt_soc);
            return result;
        }
        cBuf[result] = 0; // 末尾添加一个 '\0' 
        LogPrintf(TEXT("%s\r\n"), cBuf);
        return result;
    }
    
    static void FingerOnSocketNotify(WPARAM wParam, LPARAM lParam)
    {
        int iResult = 0;
        WORD wEvent, wError;
        wEvent = WSAGETSELECTEVENT(lParam); /* LOWORD */
        wError = WSAGETSELECTERROR(lParam); /* HIWORD */
        switch (wEvent)
        {
        case FD_CONNECT:
            if (wError)
            {
                LogPrintf(TEXT("FD_CONNECT error #%i"), wError);
                return;
            }
            FingerOnConnect(wParam);
            break;
        case FD_READ:
            if (wError)
            {
                LogPrintf(TEXT("FD_READ error #%i"), wError);
                return;
            }
            FingerOnRead(wParam);
            break;
        case FD_CLOSE:
            closesocket(wParam);
            gFingerCtrl.hSoc = INVALID_SOCKET;
            break;
        }
    }
    
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
                            WPARAM wParam, LPARAM lParam)
    {
        int cyChar, cxClient, cyClient;
        int iError;
        struct Widget *pWgt;
        switch (message)
        {
        case WM_CREATE:
            FingerOnCreate(hWnd, wParam, lParam);
            return 0;
        case WM_SIZE:
            cxClient = LOWORD(lParam);
            cyClient = HIWORD(lParam);
            cyChar = HIWORD(GetDialogBaseUnits());
            pWgt = &FgrWgt[ID_EDIT_LOG];
            MoveWindow(hWndWgt[ID_EDIT_LOG], pWgt->iLeft,
                   pWgt->iTop * cyChar, cxClient, cyClient, FALSE);
            return 0;
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        case WM_COMMAND:
            if (FingerOnCommand(hWnd, wParam, lParam))
                return 0;
            break;
        case WM_GETHOST_NOTIFY:
            iError = WSAGETASYNCERROR(lParam);
            if (iError || wParam != (WPARAM)gFingerCtrl.hAsyncHost)
            {
                gFingerCtrl.hAsyncHost = 0;
                MessageBox(hWnd, TEXT("Get Host Result Error"), NULL, 0);
                return 0; /* 发生错误 */
            }
            gFingerCtrl.hAsyncHost = 0;
            gFingerCtrl.hSoc = FingerQuery(hWnd);
            return 0;
        case WM_SOCKET_NOTIFY:
            FingerOnSocketNotify(wParam, lParam);
            return 0;
        }
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    
    
    static void LogPrintf(const TCHAR * szFormat, ...)
    {
        int iBufLen = 0, iIndex;
        TCHAR szBuffer[FINGER_MAX_BUF];
        va_list pVaList;
        va_start(pVaList, szFormat);
    #ifdef UNICODE
        iBufLen = _vsnwprintf(szBuffer, FINGER_MAX_BUF, szFormat, pVaList);
    #else
        iBufLen = _vsnprintf(szBuffer, FINGER_MAX_BUF, szFormat, pVaList);
    #endif
        va_end(pVaList);
        iIndex = GetWindowTextLength(hWndWgt[ID_EDIT_LOG]);
        SendMessage(hWndWgt[ID_EDIT_LOG], EM_SETSEL, (WPARAM)iIndex, (LPARAM)iIndex);
        SendMessage(hWndWgt[ID_EDIT_LOG], EM_REPLACESEL, FALSE, (LPARAM)szBuffer);
        SendMessage(hWndWgt[ID_EDIT_LOG], EM_SCROLLCARET, 0, 0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307

    image-20221106104818749

  • 相关阅读:
    浅析Java设计模式【3.3】——观察者
    【面试普通人VS高手系列】Dubbo的服务请求失败怎么处理?
    磁盘存储链式的B树与B+树
    VS2017+QT+PCL环境配置
    使用dasviewer加载osgb模型,不显示纹理,黑乎乎的怎么解决?
    基于PHP+MySQL的旅游景点网站的设计与开发
    MybatisPlus 中MySQL排序失效问题的解决
    【小程序项目开发-- 京东商城】uni-app之自定义搜索组件(中)-- 搜索建议
    JDK、JRE 和 JVM 的区别和联系
    HarmonyOS 4.0 实况窗上线!支付宝实现医疗场景智能提醒
  • 原文地址:https://blog.csdn.net/first_bug/article/details/127713350