In this article we will explore power of winsock api. We will implement very simple Asynchronous client and server using API without using Winsock Active-X control. Before we go into more detail let me clear why one more article on the earth for winsock API ...
Why This Article ???
You might have question that Microsoft Winsock Active-X control is great for client/server application then why should I care to learn API based Socket programming ? Here are the reasons
- If you want application without any dependancy OCX file (Winsock ocx)
- If you want to handle several connections without putting huge memory overhead. Coz when you use winsock ocx you have to use Control array to handle multiple connection and its extra overhead to maintain Array of Winsock Control.
- If you want more flexibility.
Steps to implement Winsock Server
- Initialize WSA (Winsock Service).
- Create a socket.
- Bind the socket.
- Listen on the socket.
- Accept a connection.
- Send and receive data.
- Disconnect.
Steps to implement Winsock Client
- Initialize WSA (Winsock Service).
- Create a socket.
- Connect to the server.
- Send and receive data.
- Disconnect.
In this article we will implement Winsock server. To learn about how to implement Winsock Client Please check Windows Socket (Winsock) Programming tutorial : Part-2
Now we will go through each step to construct winsock server
[1] Initialize WSA (Winsock Service)
The Very first step to implement winsock client/server is initialize winsock service. We will use StartWinsock and EndWinsock function to start and stop winsock service.
Implementing StartWinsock and EndWinsock routines |
Click here to copy the following block | Public Function StartWinsock(Optional sDescription) As Boolean Dim StartupData As WSADataType If Not WSAStartedUp Then If Not WSAStartup(&H101, StartupData) Then WSAStartedUp = True sDescription = StartupData.szDescription Else WSAStartedUp = False End If End If StartWinsock = WSAStartedUp End Function
Public Sub EndWinsock() Dim Ret& If WSAIsBlocking() Then Ret = WSACancelBlockingCall() End If Ret = WSACleanup() WSAStartedUp = False End Sub |
code Explaination
To start winsock service you must call the WSAStartup function. For each call of WSAStartup function you need to call WSACleanup in order to unload the library and clear resources allocated by the system. |
Click here to copy the following block | Declare Function WSAStartup Lib "ws2_32.dll" _ (ByVal wVersionRequired As Long, lpWSAData As WSAData) As Long
Declare Function WSACleanup Lib "ws2_32.dll" () As Long |
The wVersionRequired parameter defines highest version of Windows Sockets support that the caller can use. The high-order byte specifies the minor version (revision) number; the low-order byte specifies the major version number. Below are 2 examples of usage of the function in Visual Basic for 2 versions of the Winsock service: |
The second parameter is the WSADATA data structure that is to receive details of the Windows Sockets implementation. |
Click here to copy the following block | Type WSADataType wVersion As Integer wHighVersion As Integer szDescription As String * WSA_DescriptionSize szSystemStatus As String * WSA_SysStatusSize iMaxSockets As Integer iMaxUdpDg As Integer lpVendorInfo As Long End Type |
[2] Create a socket
Once you initialize the Winsock service the next step is create a socket which will be used to listen on a specified port. To create a new socket we canuse following VB function |
Click here to copy the following block | Public Function NewSocket(ByVal AdrFamily As Long, ByVal SckType As Long, ByVal SckProtocol As Long, HWndForMsg As Long) As Long
On Error GoTo errHandler Dim hSocket As Long Dim lngEvents As Long
hSocket = socket(AdrFamily, SckType, SckProtocol) NewSocket = hSocket
If hSocket <> INVALID_SOCKET Then lngEvents = FD_CONNECT Or FD_READ Or FD_WRITE Or FD_CLOSE Or FD_ACCEPT
lngRetValue = WSAAsyncSelect(hSocket, HWndForMsg, WINSOCKMSG, lngEvents) End If Exit Function errHandler: NewSocket = INVALID_SOCKET End Function |
code Explaination
To create a new socket you can use socket api. |
socket function expect 3 parameters : AddressFamily, SocketType and Protocol. If call is successful then it returns socket handle which you can use for subsecuent socket api calls. If function fails to create new socket then it returns -1 (SOCKET_ERROR). You can get socket error using WSAGetLastError |
Now we will discuss each input parameter for socket api.
AddressFamily:
First parameter for socket function is Addressfamily which can be one of the following options. Here we have created enum for easy use.
In our example we will use AF_INET address family with TCP protocol and STREAM socket which are most common options with windows socket. |
Click here to copy the following block | Public Enum AddressFamily AF_UNSPEC = 0
AF_UNIX = 1 AF_INET = 2 AF_IMPLINK = 3 AF_PUP = 4 AF_CHAOS = 5 AF_NS = 6 AF_IPX = AF_NS AF_ISO = 7 AF_OSI = AF_ISO AF_ECMA = 8 AF_DATAKIT = 9 AF_CCITT = 10 AF_SNA = 11 AF_DECnet = 12 AF_DLI = 13 AF_LAT = 14 AF_HYLINK = 15 AF_APPLETALK = 16 AF_NETBIOS = 17 AF_VOICEVIEW = 18 AF_FIREFOX = 19 AF_UNKNOWN1 = 20 AF_BAN = 21 AF_ATM = 22 AF_INET6 = 23 AF_CLUSTER = 24 AF_12844 = 25 AF_MAX = 26 End Enum |
SocketType:
Second parameter for socket function is SocketType. Socket type can be one of the following options. |
Protocol:
Third parameter for socket function is Protocol. Protocol can be one of the following options. |
Click here to copy the following block | Public Enum SocketProtocol IPPROTO_IP = 0 IPPROTO_ICMP = 1 IPPROTO_IGMP = 2 IPPROTO_GGP = 3 IPPROTO_TCP = 6 IPPROTO_PUP = 12 IPPROTO_UDP = 17 IPPROTO_IDP = 22 IPPROTO_ND = 77 IPPROTO_RAW = 255 IPPROTO_MAX = 256 End Enum |
There are two type of sockets programming mode available in windows. 1. Blocking socket 2. Non-Blocking socket
# Blocking socket #
For Blocking socket you have to check the status of any socket event or any error. To do that you can use select function. The select function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O. |
Click here to copy the following block | Declare Function vbselect Lib "ws2_32.dll" Alias "select" (ByVal nfds As Long _ , ByRef readfds As Any _ , ByRef writefds As Any _ , ByRef exceptfds As Any _ , ByRef timeout As Long) As Long
Public Type timeval tv_sec As Long tv_usec As Long End Type
Public Type fd_set fd_count As Long fd_array(1 To FD_SETSIZE) As Long End Type |
Parameters:
nfds [in] Ignored. The nfds parameter is included only for compatibility with Berkeley sockets.
readfds [in, out] Optional pointer to a set of sockets to be checked for readability.
writefds [in, out] Optional pointer to a set of sockets to be checked for writability.
exceptfds [in, out] Optional pointer to a set of sockets to be checked for errors.
timeout [in] Maximum time for select to wait, provided in the form of a TIMEVAL structure. Set the timeout parameter to null for blocking operations.
Return Value:
The select function returns the total number of socket handles that are ready and contained in the fd_set structures, zero if the time limit expired, or SOCKET_ERROR if an error occurred. If the return value is SOCKET_ERROR, WSAGetLastError can be used to retrieve a specific error code.
Example:
You can execute select in timer event or may be in while loop but while loop can block your thread. |
Click here to copy the following block |
Private Sub Timer1_Timer() Dim cntSocks As Long, lngRetValue As Long Dim udtRead_fds As fd_set Dim udtWrite_fds As fd_set Dim udtError_fds As fd_set
udtRead_fds.fd_array(0) = hSocket udtWrite_fds.fd_array(0) = hSocket udtError_fds.fd_array(0) = hSocket
lngRetValue = vbselect(0&, udtRead_fds, udtWrite_fds, udtError_fds, 0&) If lngRetValue = SOCKET_ERROR Then ElseIf lngRetValue > 0 Then
If udtWrite_fds.fd_count > 0 Then For i = 0 To udtWrite_fds.fd_count - 1 lngSocket = udtWrite_fds.fd_array(i) Next End If
If udtRead_fds.fd_count > 0 Then For i = 0 To udtRead_fds.fd_count - 1 lngSocket = udtRead_fds.fd_array(i) Next End If
If udtError_fds.fd_count > 0 Then For i = 0 To udtError_fds.fd_count - 1 lngSocket = udtError_fds.fd_array(i) Next End If
End If End Sub |
# Non Blocking socket #
In Non-Blocking socket windows sends you various socket events. On successful socket creation you can Register socket for asynchrous events using WSAAsyncSelect api which takes two parameters (1) socket handle (2) Event mask (socket events which you want to receive from windows). |
Parameters:
s [in] Descriptor that identifies the socket for which event notification is required.
hwnd [in] Handle that identifies the window that will receive a message when a network event occurs.
wMsg [in] Message to be received when a network event occurs. (For socket messages you have to provide WINSOCKMSG)
lEvent [in] Bitmask that specifies a combination of network events in which the application is interested.
You can create lEvent (Event Mask) parameter using OR logical operator e.g. |
Here is a summary of some events and conditions for each asynchronous notification message.
FD_READ:
- When WSAAsyncSelect is called, if there is data currently available to receive.
- When data arrives, if FD_READ is not already posted.
- After recv or recvfrom is called (with or without MSG_PEEK), if data is still available to receive.
FD_WRITE:
- When WSAAsyncSelect called, if a send or sendto is possible.
- After connect or accept called, when connection established.
- After send or sendto fail with WSAEWOULDBLOCK, when send or sendto are likely to succeed.
- After bind on a connectionless socket. FD_WRITE may or may not occur at this time (implementation-dependent). In any case, a connectionless socket is always writeable immediately after a bind operation.
FD_ACCEPT:
- When WSAAsyncSelect called, if there is currently a connection request available to accept.
- When a connection request arrives, if FD_ACCEPT not already posted.
- After accept called, if there is another connection request available to accept.
FD_CONNECT:
- When WSAAsyncSelect called, if there is currently a connection established.
- After connect called, when connection is established (even when connect succeeds immediately, as is typical with a datagram socket).
- After calling WSAJoinLeaf, when join operation completes.
- After connect, WSAConnect, or WSAJoinLeaf was called with a nonblocking, connection-oriented socket. The initial operation returned with a specific error of WSAEWOULDBLOCK, but the network operation went ahead. Whether the operation eventually succeeds or not, when the outcome has been determined, FD_CONNECT happens. The client should check the error code to determine whether the outcome was successful or failed.
FD_CLOSE: Only valid on connection-oriented sockets (for example, SOCK_STREAM)
- When WSAAsyncSelect called, if socket connection has been closed.
- After remote system initiated graceful close, when no data currently available to receive (note: if data has been received and is waiting to be read when the remote system initiates a graceful close, the FD_CLOSE is not delivered until all pending data has been read).
- After local system initiates graceful close with shutdown and remote system has responded with "End of Data" notification (for example, TCP FIN), when no data currently available to receive.
- When remote system terminates connection (for example, sent TCP RST), and lParam will contain WSAECONNRESET error value.
In order to receive Windows Messages with VB6 we will use a technique called Subclassing.To implement subclassing you have to Hook a window with Windows for additional Windows Messages which you want to receive. and when you dont want any further messages you can UnHook that
When you Hook a form or any other window then windows will start sending you messages and you can process messages which you are interested in. Before we implement Hook and UnHook lets go through some declarations for subclassing. |
Click here to copy the following block | Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long _ , ByVal nIndex As Long _ , ByVal dwNewLong As Long) As Long
Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long _ , ByVal hwnd As Long _ , ByVal Msg As Long _ , ByVal wParam As Long _ , ByVal lParam As Long) As Long
Public Const GWL_WNDPROC = (-4) Public Const WINSOCKMSG = 1025 Public PrevProc As Long |
For parameters check MSDN documentation.
Here are the steps to HOOK and UNHOOK the window.
To Hook/Unhook a window |
Click here to copy the following block | Public Sub HookForm(F As Form) PrevProc = SetWindowLong(F.hwnd, GWL_WNDPROC, AddressOf WindowProc) End Sub Public Sub UnHookForm(F As Form) If PrevProc <> 0 Then SetWindowLong F.hwnd, GWL_WNDPROC, PrevProc PrevProc = 0 End If End Sub |
To Hook a window you have to pass a handle of the window (in our case its a VB form) and you sepecify mwssagetype (in our case WINSOCK Messages) and very last one is pointer to Windows Procedure which has to be in a certain signature which windows expect(i.e number of parameter and type of parameters). Here is the implementation of Windows Procedure. |
Click here to copy the following block | Public Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long If uMsg = WINSOCKMSG Then ProcessMessage wParam, lParam Else WindowProc = CallWindowProc(PrevProc, hwnd, uMsg, wParam, lParam) End If End Function
Public Sub ProcessMessage(ByVal lFromSocket As Long, ByVal lParam As Long) Select Case lParam Case FD_CONNECT
Case FD_WRITE
Case FD_READ
Case FD_CLOSE
End Select End Sub |
[3] Bind the socket
Before you start listining on a port you have to bind your server socket with a Port and a local IP address. In this tutorial we will use the following VB function to bind a socket. |
Click here to copy the following block | Public Function SocketBind(ByVal lngSocket As Long, _ ByVal lngLocalPort As Long, _ Optional ByVal strLocalHost As String = "127.0.0.1") As Long
On Error GoTo errHandler
Dim udtSocketAddress As SOCKADDR Dim lngReturnValue As Long Dim lngAddress As Long
SocketBind = SOCKET_ERROR
With udtSocketAddress .sin_family = AF_INET
.sin_addr = GetAddressLong(strLocalHost)
.sin_port = htons(lngLocalPort) If .sin_port = INVALID_SOCKET Then SocketBind = INVALID_SOCKET Exit Function End If End With
Dim optvalue As Boolean optvalue = True If setsockopt(lngSocket, SOL_SOCKET, SO_REUSEADDR, optvalue, Len(optval)) Then ShowLog "Error setting SO_REUSEADDR option : " & WSAGetLastError() End If
SocketBind = bind(lngSocket, udtSocketAddress, LenB(udtSocketAddress))
Exit Function errHandler: SocketBind = SOCKET_ERROR End Function |
code Explaination
To bind s socket Binds the socket to a local address we can use bind api. Bind takes 3 arguments. |
Parameters:
s [in] Descriptor identifying an unbound socket.
addr [in] Address to assign to the socket from the sockaddr structure. |
namelen [in] Length of the value in the name parameter, in bytes.
Returns Values :
If no error occurs, bind returns zero. Otherwise, it returns SOCKET_ERROR, and a specific error code can be retrieved by calling WSAGetLastError.
Convert Host or IP to long
SOCKADDR structure requires sin_addr which is Long. To retrive Long number from IP or Host name we can use following helper function. |
Click here to copy the following block | Function GetHostByNameAlias(ByVal hostname$) As Long On Error Resume Next Dim phe& Dim heDestHost As HostEnt Dim addrList& Dim retIP&
retIP = inet_addr(hostname)
If retIP = INADDR_NONE Then phe = gethostbyname(hostname) If phe <> 0 Then MemCopy heDestHost, ByVal phe, hostent_size MemCopy addrList, ByVal heDestHost.h_addr_list, 4 MemCopy retIP, ByVal addrList, heDestHost.h_length Else retIP = INADDR_NONE End If End If GetHostByNameAlias = retIP If Err Then GetHostByNameAlias = INADDR_NONE End Function |
[4] Listen on the socket
After successful socket bind you can call listen function so server can accept incoming connection requests. We will use following VB function to start listen. |
Click here to copy the following block | Function StartListen(hSocket As Long, Port As Long) As Long
Dim lngRetValue As Long
If SocketBind(hSocket, Port) = SOCKET_ERROR Then StartListen = SOCKET_ERROR lngRetValue = listen(hSocket, SOMAXCONN) StartListen = lngRetValue End Function |
code Explaination
Listen function takes 2 parameters. |
Click here to copy the following block | Declare Function listen Lib "ws2_32.dll" ( _ ByVal s As Long, _ ByVal backlog As Long) As Long
Declare Function gethostbyname _ Lib "ws2_32.dll" (ByVal host_name As String) As Long
Declare Function inet_addr _ Lib "ws2_32.dll" (ByVal cp As String) As Long
Declare Function htons _ Lib "ws2_32.dll" (ByVal hostshort As Integer) As Integer |
Parameters:
s [in] Descriptor identifying a bound, unconnected socket.
backlog [in] Maximum length of the queue of pending connections. If set to SOMAXCONN, the underlying service provider responsible for socket s will set the backlog to a maximum reasonable value. There is no standard provision to obtain the actual backlog value.
We can use following declaration |
Return Values :
If no error occurs, listen returns zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.
[5] Accept a connection
Once socket goes into listining mode you can start accepting new connection requests from client. To accept connection request you can use following VB function to make your interface more clean. You can call SocketAccept function when you receive FD_ACCEPT event which means that client is waiting for connection. Once server accept incoming request for new connection client will receive FD_CONNECT event. |
Click here to copy the following block | Public Function SocketAccept(ByVal lngSocketHandle As Long) As Long
Dim lngRetValue As Long Dim udtSocketAddress As SOCKADDR Dim lngBufferSize As Long lngBufferSize = LenB(udtSocketAddress)
lngRetValue = accept(lngSocketHandle, udtSocketAddress, lngBufferSize)
SocketAccept = lngRetValue If lngRetValue <> INVALID_SOCKET Then colConnections.Add lngRetValue End Function |
code Explaination
accept function takes 3 parameters. |
Parameters:
s [in] Descriptor that identifies a socket that has been placed in a listening state with the listen function. The connection is actually made with the socket that is returned by accept.
addr [out] Optional pointer to a buffer that receives the address of the connecting entity, as known to the communications layer. The exact format of the addr parameter is determined by the address family that was established when the socket from the sockaddr structure was created.
addrlen [in, out] Optional pointer to an integer that contains the length of addr.
Return Values
If no error occurs, accept returns a value of type SOCKET that is a descriptor for the new socket. This returned value is a handle for the socket on which the actual connection is made. Otherwise, a value of INVALID_SOCKET is returned, and a specific error code can be retrieved by calling WSAGetLastError.
[6] Send and receive data
After connection is established you can start send/receive data. We will use Send following functions for this purpose. |
Click here to copy the following block | Dim strData As String Dim ReadBuffer(1 to 1024) As Byte
Public Function SendData(ByVal lToSocket As Long, vMessage As Variant) As Long Dim TheMsg() As Byte, sTemp$ TheMsg = "" Select Case VarType(vMessage) Case 8209 sTemp = vMessage TheMsg = sTemp Case 8 sTemp = StrConv(vMessage, vbFromUnicode) Case Else sTemp = CStr(vMessage) sTemp = StrConv(vMessage, vbFromUnicode) End Select TheMsg = sTemp If UBound(TheMsg) > -1 Then SendData = send(lToSocket, TheMsg(0), (UBound(TheMsg) - LBound(TheMsg) + 1), 0) End If End Function
Function RecvDataToBuffer(lFromSocket As Long) As Long Dim X As Long Do X = recv(lFromSocket, ReadBuffer(1), 1024, 0) If X > 0 Then strData = strData & Left$(StrConv(ReadBuffer, vbUnicode), X) End If If X <> 1024 Or X = SOCKET_ERROR Then Exit Do Loop RecvDataToBuffer = X End Function |
code Explaination
Send function takes 4 parameters |
Parameters:
s [in] Descriptor identifying a connected socket.
buf [in] Buffer containing the data to be transmitted.
len [in] Length of the data in buf, in bytes.
flags [in] Indicator specifying the way in which the call is made.
Return Values
If no error occurs, send returns the total number of bytes sent, which can be less than the number indicated by len. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.
recv function takes 4 parameters |
Parameters:
s [in] The descriptor that identifies a connected socket.
buf [out] The buffer for incoming data.
len [in] The length, in bytes, of buf.
flags [in] The flag that specifies the way in which the call is made.
Return Values
If no error occurs, recv returns the number of bytes received. If the connection has been gracefully closed, the return value is zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.
[7] Disconnect
To assure that all data is sent and received on a connected socket before it is closed, an application should use shutdown to close connection before calling closesocket. For example, to initiate a graceful disconnect:
- Call WSAAsyncSelect to register for FD_CLOSE notification.
- Call shutdown with how=SD_SEND.
- When FD_CLOSE received, call recv until zero returned, or SOCKET_ERROR.
- Call closesocket.
|
Click here to copy the following block | Declare Function shutdown Lib "ws2_32.dll" ( _ ByVal s As Long, _ ByVal how As Long) As Long
Declare Function closesocket Lib "ws2_32.dll" ( _ ByVal s As Long) As Long |
|