NTT Information Sharing Platform Laboratories
Introduction
In Part 4, I will explain how to handle sockets that use asynchronous I/O. The TCP echo programs that I previously wrote about, both servers and clients, ran in a console. For programmers with experience in UNIX programming, I think it was probably easy to understand. This time, in order to create a program that behaves more like a Windows program, we will build a TCP echo client that is operated via GUI. I will focus on the processing of window messages using the asynchronous access model, particularly for sockets.
Most of the programs that run on Windows usually have a GUI and use an event-driven model. Suppose an application consists of a main window and a button, as shown in Figure 1.
Figure 1: Events and Window Procedure Calls
[0]
First, register the window procedure (callback function) for the main window that will be called when an event occurs. When an event occurs inside the window, the window procedure is called. We can use some GUI-related operations as examples of these events, e.g. “a button was clicked”, “the mouse pointer entered the window”, “the window was resized”, “the x button was clicked in order to shut down the application”, etc. When an event occurs, the callback function is called asynchronously during program operation. When this happens, a window message is sent to the window procedure in order to let it know what kind of event has occurred. The window procedure checks the content of the window message after receiving it and decides on what action the application will take. For example, if it receives a message that indicates “a button was clicked”, it will execute the action that was set up for when a button is clicked.
About synchronous sockets
All socket I/O that we covered up until Part 3 was based on an access model called synchronous (blocking) sockets. Suppose that network and server load are significantly high and that the server connection takes close to 1 minute from start to finish. This means that with synchronous sockets, you have to wait 1 minute after the completion of process 1 in Figure 2 for process 2 to start.
Figure 2: The Outline of the Process When Blocking Occurs

In a GUI program, this blocking in the socket process causes a big problem. For example, while socket I/O is happening, it is possible for the GUI to completely freeze since other processes are not running. For example, consider the following situation. Suppose it takes 1 minute to connect to the server after inputting a URL in the address bar of a web browser and hitting the Enter key. If all the GUI operations are frozen during that minute and you cannot click on the cancel button for loading or even resizing and moving the window, the program’s usability as a GUI application is significantly compromised. So, it is necessary to multiplex the socket I/O and GUI processes to carry out the processes at the same time.
The relationship between asynchronous I/O socket access and window messages
Windows provides a socket access model that uses asynchronous I/O. If you use this model, even when a socket function is called, blocking does not occur. The socket function returns immediately after it is called. In Figure 3, the connect() function, called after Process 1, returns immediately even if it is taking time to connect (in other words, even while it is in the middle of connecting).
Figure 3: The Outline of the Process using Non-Blocking Socket

However, if the function returns in the middle of the process, you cannot tell if the connection process succeeded or failed. So, WinSock sends a window message that contains the result of the connection process to the application asynchronously when the connection() function completes. By having a window procedure receive and check the message, it is possible to know the result of the connect() function.
Figure 4 shows an example of an asynchronous socket process. This is an application that begins connecting to the server when the button is clicked. When the button is clicked, a message indicating that “the button was clicked” is sent to the window procedure (j). The process to begin connecting that is set up for the button is called (k). Although the connection to the server is being processed, the connect() function immediately returns and gives control back to the GUI process (l). Once the connection process completes, an event indicating that the process completed is fired and the window procedure is called once again (m). As described above, GUI and socket I/O events can be handled in a completely integrated manner, and you can see that the access model using asynchronous I/O is compatible with GUI programs.
Figure 4: An Example of an Asynchronous Socket Process
[0]
Behavior of each function when using asynchronous sockets
The window messages shown below are generated by socket I/O when an asynchronous socket is used. You can choose either synchronous or asynchronous depending on the kind of I/O. For example, you can make it so that the connect() function runs asynchronously and other functions run synchronously and block.
- FD_CONNECT
This event fires when the connect() function completes the connection process. You can find out the result of the connection process by checking the window message contents (parameter) in the window procedure. - FD_WRITE
Even when a synchronous socket is used, there are not many cases where the send() function blocks. This is because the role of the send() function is to copy data from the application to the send buffer of the protocol stack and it does not send the data over the network. (see endnote 1 [0])
However, suppose you tried to send 200 bytes of data but only 100 bytes of the data could be copied to the send buffer because the send buffer space was smaller that it should be for some reason. When this happens, once some space becomes available in the send buffer of the protocol stack after a while, a window message indicating that there is space available is generated. - FD_READ
With synchronous sockets, if the recv() function is called when there is no data in the receive buffer of the protocol stack, it will continue to wait until the data arrives. As for asynchronous sockets, even when there is no data in the buffer, control immediately returns. Once the data arrives in the receive buffer, a window message indicating that the data has arrived will be generated. - FD_CLOSE
The communication partner can disconnect the session using the closesocket() function, etc. and this can happen asynchronously during your program’s operation. When your program detects a disconnection from the communication partner, this event occurs.
Core design of a TCP echo client using asynchronous I/O
Figure 5: Screenshot of Asynchronous I/O TcpEchoClient

Figure 5 is a screenshot of the TCP echo client that we will create this time. This application sends a character string written in the String edit box to the TCP echo server and shows the character string that was sent back (echoed back) at the bottom of the screen. The trigger to begin sending is the SEND button.
Figure 6 shows the rough outline of the program after the SEND button has been clicked up until it shows the echoed back character string on the screen. To keep things simple, only the connect() and recv() functions run asynchronously.
Figure 6: Rough Outline of an Asynchronous I/O TCP Echo Client
[0]
The rough outline from the window message generated by the SEND button being clicked up until the communication completes is shown in (1)~(8).
- SEND button is clicked
- Execute “name resolution”. Obtain the host name from the Server edit box and move onto executing the connection process based on the result of the name resolution
- Immediately return to the GUI process without waiting for the result of the connection process
- When the connection process (connect() function) completes, an event occurs and an applicable process in the window procedure will be called
- Execute the send process (send() function). Obtain the character string to send from the String edit box
- Data arrives in the receive buffer because it was echoed back from the server. An event indicating that data has arrived occurs and an applicable process in the window procedure will be called
- Execute the receive process (recv() function) and extract the character string from the receive buffer
- Render the received character string on the screen and returns to the GUI process
Structure of window messages
Now I will explain the structure of window messages generated by socket I/O operations. Window messages can take two parameters besides the message classification. Figure 7 shows the parameter structure of window messages generated by socket I/O operations. When a socket I/O operation occurs, a WM_SOCKET (see endnote 2 [0]) message is generated.
Figure 7: Parameter Structure of Window Messages Generated by Socket I/O Operations
[0]
The information on what kind of I/O occurred in which socket and if it was a success or failure will be sent using two parameters (wParam, lParam) at this time. The data structures for wParam and lParam are different depending on the kind of window message. When the message is about socket I/O, you can extract the applicable socket using
SOCKET s = (SOCKET)wParam;
and extract the following:
WSAGETSELECTERROR(lParam) macro to get error status WSAGETSELECTEVENT(lParam) macro to get message content |
Source code of the asynchronous I/O TCP echo client and how to compile it Listing 1 shows the entire source code of the asynchronous I/O TCP echo client; Figure 8 shows how to compile it. Since this is a program that uses a GUI, user32.lib and comctl32.lib are linked as import libraries. The latter, common controls, is used to display the status bar. Window initialization during application startup • Lines 85-103. On Windows, application execution starts with the WinMain() function. In this function, WinSock initialization (line 94: WSAStartup() function), window class registration (line 98: InitApplication() function), and window creation and display (line 102: InitInstance() function) are performed. |
080:/** Entry point of Windows program */
081:/*
082: * Enter the main loop in order to initialize WinSock
083: * and process messages after GUI initialization
084: */
085:int WINAPI WinMain(HINSTANCE hInstance,
086: HINSTANCE hPrevInstance,
087: LPSTR lpCmdLine,
088: int nCmdShow)
089:{
090: WSADATA wsaData;
091: MSG msg;
092:
093: /* Initialize WinSock */
094: if (WSAStartup(MAKEWORD(2, 2), &wsaData))
095: return FALSE;
096:
097: /* Register Window Class */
098: if (!InitApplication(hInstance))
099: return FALSE;
100:
101: /* Create GUI window and display */
102: if (!InitInstance(hInstance, nCmdShow))
103: return FALSE;
.
.
112:} |
|
115:/** Process to register the window class */
116:BOOL InitApplication(HANDLE hInstance)
117:{
118: WNDCLASS wc;
119:
120: wc.style = CS_HREDRAW | CS_VREDRAW;
121: /* Register the window procedure */
122: wc.lpfnWndProc = (WNDPROC)WndProc;
123: wc.cbClsExtra = 0;
124: wc.cbWndExtra = 0;
125: wc.hInstance = hInstance;
126: wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
127: wc.hCursor = LoadCursor(NULL, IDC_ARROW);
128: wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW;
129: wc.lpszMenuName = NULL;
130: wc.lpszClassName = TEXT("Async I/O TcpEchoClient");
131:
132: return RegisterClass(&wc);
133:}
|
|
136:/** Process to create the window and display */
137:BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
138:{
139: HWND hWnd;
140:
141: g_hInst = hInstance; // Store the instance in a global variable.
142:
143: /* Create the parent window */
144: hWnd = CreateWindow(TEXT("Async I/O TcpEchoClient"), ...);
.
157: /* Create the controls within the parent window */
158: hServerLabel = CreateWindow(TEXT("STATIC"), TEXT("Server"), ...);
166: hServerEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
176: hSendButton = CreateWindow(TEXT("BUTTON"), TEXT("SEND"), ...);
185: hStringLabel = CreateWindow(TEXT("STATIC"), TEXT("String"), ...);
193: hStringEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
203: hRecvEdit = CreateWindowEx(WS_EX_CLIENTEDGE, ...);
217: hStatusLabel = CreateWindowEx(0, ...);
.
.
.
228: SetFocus(hServerEdit);
229:
230: ShowWindow(hWnd, nCmdShow); /* Display the window */
231: UpdateWindow(hWnd); /* Update the window for the first time */
232:
233: return TRUE;
234:} |
|
085:int WINAPI WinMain(HINSTANCE hInstance,
086: HINSTANCE hPrevInstance,
087: LPSTR lpCmdLine,
088: int nCmdShow)
089:{
.
.
.
105: /* Process window messages */
106: while (GetMessage(&msg, NULL, 0, 0)) {
107: TranslateMessage(&msg);
108: DispatchMessage(&msg);
109: }
110:
111: return (int)msg.wParam;
112:} |
• Lines 242-254. Next, suppose the user clicked on the SEND button. Suppose also that the server name is written in the Server edit box and a character string to be sent in is written in the String edit box. When the SEND button is clicked, an event (WM_COMMAND) occurs and the window procedure, WndProc(), is called. If you check the window message contents (uMsg) in line 251, you will see it is a button click (WM_COMMAND). So, the process will move forward to the fork in line 252, and the name of the button is specified in lines 253-254. |
237:/** Main body of the window procedure */
238:/*
239: * When a button click or socket I/O event occurs,
240: * a window message is sent via the uMsg variable.
241: */
242:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
243:{
.
.
250: /* Determine the action to take depending on what window message was received */
251: switch (uMsg) {
252: case WM_COMMAND:
253: switch (LOWORD(wParam)) {
254: case ID_SEND: |
|
255: /* Disable the SEND button */
256: EnableWindow(hSendButton, FALSE);
257:
258: /* Display the “resolving server name” status in the status bar */
259: SendMessage(hStatusLabel, SB_SETTEXT,
260: (WPARAM)(255 | 0),
261: (LPARAM)TEXT("Status: resolving servername")); |
|
263: /* Extract the server name from the Server edit box */
264: SendMessage(hServerEdit, WM_GETTEXT,
265: (WPARAM)sizeof(buf), (LPARAM)buf);
266: if (strlen(buf) == 0) {
267: NotifyError(hWnd, TEXT("invalid server name or address"));
268: break;
269: } |
|
271: /* Resolve the server name */
272: memset(&hints, 0, sizeof(hints));
273: hints.ai_family = AF_UNSPEC;
274: hints.ai_socktype = SOCK_STREAM;
275: if (getaddrinfo(buf, "echo", &hints, &g_ai0)) {
276: NotifyError(hWnd, TEXT("server not found"));
277: break;
278: } |
•Lines 281-283. If name resolution succeeded, the program moves onto connecting. First, display the “connecting” status, which indicates the connect() function is running, in the status bar at the bottom of the GUI. |
280: /* Display the “connecting” status in the status bar */
281: SendMessage(hStatusLabel, SB_SETTEXT,
282: (WPARAM)(255 | 0),
283: (LPARAM)TEXT("Status: connecting server")); |
Now, the g_ai variable is treated as the item of the ADDRINFO list that the program is currently attempting to connect to. For its initial value, set the top item (g_ai0) of the list that was returned by the getaddrinfo() function. After this, connection attempts will be processed within the ConnectServer() function, starting with the top item of the ADDRINFO list. The ConnectServer() function returns a BOOL value of either TRUE or FALSE. When TRUE, it means either that the connect() process hasn’t completed or that there are ADDRINFO structure items left after the current g_ai. In other words, if TRUE is returned, it is either waiting for the result of the connection process or there are targets left to attempt to connect to, so it is not treated as a fatal error. On the other hand, if FALSE is returned, there is no ADDRINFO structure item left to attempt to connect to after g_ai. This happens when no connection could be made, even when the fallback from IPv6 to IPv4 was attempted on all the targets. When FALSE is returned, it is treated as a fatal error and the program returns to its initial state. |
285: /* Begin connecting */
286: g_ai = g_ai0;
287: if (ConnectServer(hWnd) == FALSE) {
288: NotifyError(hWnd, TEXT("connect error"));
289: freeaddrinfo(g_ai0);
290: break;
291: }
292:
293: break; |
|
423:/** Process to connect to the server */
424:/* Return value
425: * TRUE: When connection is successful or when the result is not confirmed
426: since the connection is still in progress. In the latter case,
427: confirm the result by checking the result of the window message
428: (FD_CONNECT) within the window procedure, WndProc.
429: FALSE: Failed in attempts to connect to all the items in the ADDRINFO list g_ai0.
430: If FALSE is returned, it means that the fallback from IPv6 to IPv4
431: completely failed as well.
432: */
433:BOOL ConnectServer(HWND hWnd)
434:{
435: SOCKET s;
436:
437: if (g_ai == NULL)
438: return FALSE;
.
.
.
478:} |
As for WM_SOCKET, the programmer defines it based on the user definable window message (line 47). |
046:/** Definition of window messages related to socket I/O */
047:#define WM_SOCKET (WM_USER+1)
.
.
.
440: for ( ; g_ai; g_ai = g_ai->ai_next) {
441:
442: /* Create a socket */
443: s = socket(g_ai->ai_family, g_ai->ai_socktype, g_ai->ai_protocol);
444: if (s == INVALID_SOCKET)
445: continue;
446:
447: /* Make the socket asynchronous */
448: if (WSAAsyncSelect(s, hWnd, WM_SOCKET,
449: FD_CONNECT | FD_READ) == SOCKET_ERROR) {
450: closesocket(s);
451: s = INVALID_SOCKET;
452: continue;
453: } |
•Lines 456-468. Processing the connect() function is also an important part of asynchronous I/O. At a glance, it looks mostly the same as the process for synchronous socket. However, no blocking occurs in the connect() function; it immediately returns even when it is in the middle of connecting. Even if the connect() function returns an error (SOCKET_ERROR) as the return value, the program checks the details of the error using the WSAGetLastError() function (see “Differences between WinSock and Berkeley sockets” [0]). If the error is WSAEWOULDBLOCK, it means that the connect() function is still running in the background. Since the result of the connection process is unknown at this time, do not treat it as an error. Return from the ConnectServer() function without doing anything, and then wait for the result of the connect() function (a window message from WinSock (WM_SOCKET) ). Once the ConnectServer() function returns, the program immediately leaves the window procedure (WndProc() function) as well, so it will immediately return to the event processing loop in lines 106-109. |
455: /* Connection process */
456: if (connect(s, g_ai->ai_addr, g_ai->ai_addrlen) == SOCKET_ERROR)
457: /* Even if an error is returned, if the error code is WSAEWOULDBLOCK, */
458: /* the connection process is still running in the background. */
459: /* Wait for the window message (FD_CONNECT) to arrive. */
460: /* If the error message is anything else, stop the process */
461: /* on this g_ai. */
462: if (WSAGetLastError() != WSAEWOULDBLOCK) {
463: closesocket(s);
464: s = INVALID_SOCKET;
465: continue;
466: }
467:
468: break; |
When the connect() process is completed, a WM_SOCKET window message is generated. It takes two parameters and they are represented as the wParam and lParam variables. We will take a look at how they are processed by the window procedure (WndProc() function). • Lines 300-317. Since a socket descriptor is passed into the wParam variable, you can extract the descriptor by casting it to the SOCKET type as shown in line 303. Also, in lines 309-310, the program determines if the socket I/O operation was a success or failure. If the value of the WSAGETSELECTERROR(lParam) macro is other than 0, it means the I/O operation failed. The WSAGETSELECTEVENT(lParam) macro can extract the type of I/O operation. Normally, when an error occurs in the socket I/O, the process is stopped and the program returns to its initial state. However, the connect() function continues processing even when an error occurs (line 310). This is so that we can determine the result of the connection process inside of the fork after line 316 in order to fall back to IPv4 (if it cannot connect with IPv6, then it will try connecting with IPv4). |
242:LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
243:{
.
251: switch (uMsg) {
.
300: case WM_SOCKET:
301: /** An event related to socket I/O occurred*/
302:
303: /* Extract the socket descriptor */
304: s = (SOCKET)wParam;
305:
306: /* Check the process result of socket I/O operations other than the connect() function */
307: /* When WSAGETSELECTERROR(lParam) is not 0 (failed), */
308: /* return to the initial state. */
309: if ( WSAGETSELECTERROR(lParam) != 0
310: && WSAGETSELECTEVENT(lParam) != FD_CONNECT) {
311: closesocket(s);
312: NotifyError(hWnd, TEXT("socket error"));
313: break;
314: }
315:
316: switch (WSAGETSELECTEVENT(lParam)) {
317: case FD_CONNECT: |
|
318: /** Received notice that connection process completed. */
319:
320: if (WSAGETSELECTERROR(lParam) != 0) {
321: /** Since the connect() function failed, close the socket and reattempt. */
322:
323: /* Close the socket that could not connect. */
324: closesocket(s);
325:
326: /* Reattempt connection and wait for a window message (FD_CONNECT) again. */
327: g_ai = g_ai->ai_next;
328: if (ConnectServer(hWnd) == FALSE) {
329: closesocket(s);
330: NotifyError(hWnd, TEXT("connect error"));
331: freeaddrinfo(g_ai0);
332: break;
333: }
334:
335: break;
336: } |
|
338: /** If the process has reached this line, */ 339: /** the connect() function was successful. */ 340: 341: /* Free the ADDRINFO list. */ 342: freeaddrinfo(g_ai0); |
•Lines 345-356. We now move onto the process of sending a character string to the server. First, change the status bar status, extract the character string to send from the String edit box and store it in the buf variable. |
344: /* Display “sending” status in the status bar. */
345: SendMessage(hStatusLabel, SB_SETTEXT,
346: (WPARAM)(255 | 0),
347: (LPARAM)TEXT("Status: sending string"));
348:
349: /* Obtain the character string from the String edit box to send to the server. */
350: g_StringLen = SendMessage(hStringEdit, WM_GETTEXT,
351: (WPARAM)sizeof(buf), (LPARAM)buf);
352: if (g_StringLen == 0) {
353: closesocket(s);
354: NotifyError(hWnd, TEXT("empty string"));
355: break;
356: } |
|
358: /* Send the character string obtained from the edit box to the server. */
359: for (len = 0; len < g_StringLen; ) {
360: n = send(s, buf + len, g_StringLen - len, 0);
361: if (n == SOCKET_ERROR) {
362: closesocket(s);
363: NotifyError(hWnd, TEXT("send Error"));
364: break;
365: }
366: len += n;
367: }
368:
369: break; |
Once the sending of the character string to the server is completed, the same character string will be echoed back from the server. Since GUI processing also needs to be performed at all times, the client program cannot concentrate on waiting for the data to arrive. Until the data is echoed back from the server, the program continues the event processing loop in lines 106-109 and also watches for things such as GUI actions. •Lines 251-372. When a character string is received from the server and the protocol stack puts data in the receive buffer, WinSock generates a window message WM_SOCKET (FD_READ type) that indicates this. When the window procedure (WndProc() function) receives it, it switches to the process below line 372 through the fork in line 371. |
251
: switch (uMsg) {
300: case WM_SOCKET:
.
316: switch (WSAGETSELECTEVENT(lParam)) {
.
371: case FD_READ:
372: /** Received notification that there is data in the receive buffer. */
.
. |
|
374: /* Display the “receiving” status in the status bar. */
375: SendMessage(hStatusLabel, SB_SETTEXT,
376: (WPARAM)(255 | 0),
377: (LPARAM)TEXT("Status: reciving string")); |
|
379: /* Receive data from the receive buffer. */
380: n = recv(s, buf + g_RecvLen, g_StringLen - g_RecvLen, 0);
381: if (n == 0 || n == SOCKET_ERROR) {
382: closesocket(s);
383: NotifyError(hWnd, TEXT("recv error"));
384: }
385:
386: /* Check if all the data has arrived. */
387: g_RecvLen += n;
388: if (g_RecvLen < g_StringLen)
389: return 0; |
|
391: /* Display the received character string on the screen. */
392: buf[g_RecvLen] = '\0';
393: SendMessage(hRecvEdit, WM_SETTEXT, (WPARAM)0, (LPARAM)buf);
394:
395: /* Close the socket and end the communication. */
396: WSAAsyncSelect(s, hWnd, WM_SOCKET, 0);
397: closesocket(s);
398: EnableWindow(hSendButton, TRUE);
399: SendMessage(hStatusLabel, SB_SETTEXT,
400: (WPARAM)(255 | 0), (LPARAM)TEXT(""));
401: g_RecvLen = 0;
402:
403: break; |
Necessity of making the name resolution process asynchronous If you ran the sample program, you may have noticed that the GUI freezes for a moment during the name resolution that is performed right after the SEND button is clicked. The GUI does not permit any operations while the status bar displays “Status: resolving servername”. This is because the entire application is blocked while the getaddrinfo() function is being called and GUI-related processes cannot be performed. In the WinSock API, APIs that resolve names without blocking, such as the WSAAsyncGetHostByName() function, are provided by default and it is a rule that any program that uses asynchronous sockets also uses asynchronous API for name resolution. However, since there is no IPv6-ready API, you need to write your own from scratch using a synchronous name resolution function and threads. Although I did not explain it this time, I would like to explain how to create asynchronous getaddrinfo()/getnameinfo() functions on another occasion. Conclusion In Part 4, I focused on multiplexing models using asynchronous I/O. Since the code for the GUI part of the TCP echo client that I introduced this time became large, at first glance it may have appeared to be a complicated program. However, if you look only at the socket I/O, it is highly compatible with a GUI program because the asynchronous I/O access model and window messages generated by the GUI can be handled in an integrated manner. Also, although asynchronous I/O is specific to Windows, there are not any expressions that are dependent on a specific protocol at all. It has a structure that can support any protocol, from IPv6 and IPv4 to ones that may emerge in the future, by just adding a fallback process around the connect() function. In the next article, I will explain how to create a TCP echo server using asynchronous I/O. Listing 1:tcp-echo-client-asyncio.c [0] |
001:/* 002: * how to compile: 003: * C:???rcl tc-pecho-client-asyncio.c user32.lib comctl32.lib ws2_32.lib 004: */ 005: 006:#define STRICT 007:#include ‹winsock2.h› 008:#include ‹ws2tcpip.h› 009:#include ‹windows.h› 010:#include ‹commctrl.h› 011: 012:/** GUI-related definitions */ 013:/* 014: * GUI setup 015: * 016: * +----------------------------------------------+ |
Figure 8: How to compile tcp-echo-client-asyncio.c |
C:\>cl tcp-echo-client-asyncio.c user32 comctl32.lib ws2_32.lib Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80x86 Copyright (C) Microsoft Corporation 1984-2002. All rights reserved. tcp-echo-client-asyncio.c Microsoft (R) Incremental Linker Version 7.10.3077 Copyright (C) Microsoft Corporation. All rights reserved. /out:tcp-echo-client-asyncio.exe tcp-echo-client-asyncio.obj user32.lib comctl32.lib ws2_32.lib |
|