Windows Sockets | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||
Download: |
|
|||||||||||
There's no need to specify "Windows Sockets Application" when creating your Project with AppWizard for any of these classes. Windows Sockets allow you to get data between networked computers whether the network is a LAN, WAN or the Internet. Even the simplest examples seen elsewhere are too difficult for casual use, and cause people problems when trying to use Sockets in separate Threads. Windows Sockets can get as difficult as you'd like to make them... it took about two months to evolve these classes! |
Ultimately they send and receive data through CArchives. This is discussed later.
CSocketClient lets you create a Client Application. That means that your Application talks and listens to exactly one other Computer which the user (or code) has to specify by IP address (or Computer Name) and TCP Port. This may be a one to one, Peer to Peer arrangement, or your application may be a Client which is talking to a Server which is handling thousands of other Clients at the same time. Each CSocketClient instance makes one Socket connection.
You'd normally use Send("Msg"); and Msg=GetReply(); to send and receive Messages.
CSocketServer and CConnection allow your Application to talk and listen to many other computers at the same time. This is Server or full Peer to Peer behaviour. CSocketServer listens for connections on a particular TCP Port. It does this in a separate Thread, so your Application can carry on with other work. When a Client requests a Connection, an instance of CConnection is created and added to a list of classes held by CSocketServer. CConnection is derived from CSocketBase and behaves in a similar fashion to CSocketClient except that each instance of CConnection is running in a separate Thread. This means that if one instance of CConnection hangs for some reason, the Server should continue to function.
This code does not use I/O Completion Ports. If you are developing an application which will ultimately have thousands of connections then you need to use I/O Completion Ports and/or many servers. I/O Completion Ports use a pool of 50 or more Worker Threads... If you have a peak of fewer than 50 simultaneous Connections then this code (using one Thread per Connection) will use fewer System Resources and be far faster to code!
By default the Port is 14 which not a Standard Port (There shouldn't be anything else listening on this Port). Ports over 1024 are usually free for new Applications. Standard Ports are listed in your Windows\system32\drivers\etc\SERVICES file.
You may get a TRACE:"Warning - Attempt made to seek on a CSocketFile" from these routines: its just the CArchives closing down.
Be aware that if the socket uses an Internet Connection it is subject to the same restrictions as e-mails and Web Pages:
Binary data may be altered by Routers, so it should be sent using UTF-8 encoding:
If you are sending text with a few strange characters, use Quoted Printable.
If you are sending Binary Data use Base64 or an equivalent of your own design (UU encoding is less efficient).
CArchive remembers every object it saves (so that it only saves a reference if it gets the same object twice)... Is this is true for CArchives linked to CSocketFiles? Just in case, stop this behaviour (which is obviously wrong for a Socket transmission) by using the following instead of CArchive if you're sending Objects through CArchive:
class CSocketArchive : public CArchive { public: CSocketArchive(CFile* pFile, UINT nMode, int nBufSize=4096) : CArchive(pFile, nMode, nBufSize) {}; void ClearIndex() { if(IsStoring()) { if(m_nMapCount>1) { m_pStoreMap->RemoveAll(); m_pStoreMap->SetAt(NULL,(void*)(DWORD)0); m_nMapCount=1; } }else while(m_nMapCount-->1) m_pLoadArray->RemoveAt(m_nMapCount); } };
CSocketClient
This is the standard Client class.
It allows you to connect to a Port on a specific Server;
Call Send(...) to start a conversation;
When you expect a reply use GetReply():
CSocketClient Chat("127.0.0.1", 23); Chat.Send("Hi"); if(Chat.GetReply()=="bye") Chat.Disconnect(); else Chat.Send("bye");If the Server starts a conversation,
the overridable function OnReceive() is called
from where you may use GetReply() and Send(...) again.
Here's a Chat Server Client example:
Create a Dialog Application with a ListBox to print messages in, an EditBox to Type Messages in, and a [Send] Button.
The GetReply and Send overrides alter the default behaviour from sending CStrings to ordinary strings. Making the Client interchangeable with a Telnet session.
#include "SocketInterface.h" class CClientDlg : public CDialog { ... public: class CChatClient : public CSocketClient { CClientDlg* Dlg; void OnReceive() {Dlg->Say(GetReply());} void OnClose () {Dlg->Disconnect();} CString GetReply() { CString S; ArI->ReadString(S); // It'll wait here for as long as the connection is OK. If the connection is broken it returns an empty string. return S; } public: void Send(const CString& Msg) {ArO->WriteString(Msg+"\r\n"); ArO->Flush();} CString Connect(const CString& RemoteIP, BYTE Port, CClientDlg* _Dlg) {Dlg=_Dlg; return CSocketClient::Connect(RemoteIP,Port);} } Chat; void Say(const CString& Msg) {if(::IsWindow(m_hWnd)) ((CListBox*)GetDlgItem(IDC_LIST1))->AddString(Msg);} void Disconnect() { Say("Disconnected"); GetDlgItem(IDC_Send)->EnableWindow(FALSE); } ... }; BOOL CClientDlg::OnInitDialog() { CDialog::OnInitDialog(); CString S(Chat.Connect("127.0.0.1",23, this)); // Note that you can't connect to yourself: You'll need to be running a Server on this PC to get a connection on 127.0.0.1 if(!S.IsEmpty()) Say(S); // Report any Error Messages return TRUE; // return TRUE unless you set the focus to a control } // When a Send Button is clicked, the text from an Edit Box (IDC_EDIT1) is sent. void CClientDlg::OnSend() { CString S; GetDlgItemText(IDC_EDIT1, S); Chat.Send(S); }
CSocketServer
Start by deriving a class from CConnection and overriding the virtual functions to encapsulate the behaviour you want for one Connection.
Since Telnet is a standard application on Windows, a Telnet Chat Server can be made as an example.
The Server will accept connections from Telnet or from the client described above using CSocketClient.
Create a Dialog Application with a ListBox to print messages in, an EditBox to Type Messages in, and a [Broadcast] Button.
The Server needs a pointer to the Dialog Box to give textual output to be displayed in a ListBox.
Derive a class from CSocketServer called CChatServer which simply holds a pointer to the Dialog and provides a Say(...) function that will add text to a List Box:
#include "SocketInterface.h" class CChat; // <- forward references class CServerDlg; class CChatServer : public CSocketServer { CServerDlg* Dlg; public: CChatServer(BYTE Port, CServerDlg* Dlg) : CSocketServer(Port), Dlg(Dlg) {} void Say(const CString& Msg); protected: virtual CConnection* New(); }; void& CChatServer::Say(const CString& Msg) {Dlg->Say(Msg);} CConnection* CChatServer::New() {return new CChat(this);} class CServerDlg : public CDialog { CChatServer* Chat; public: CServerDlg(CWnd* pParent=NULL) : CDialog(CServerDlg::IDD, pParent), Chat(0) {} // standard constructor virtual ~CServerDlg(); void Say(CString Msg) {if(::IsWindow(m_hWnd)) ((CListBox*)GetDlgItem(IDC_LIST1))->AddString(Msg);} ... };After the Dialog class is the Handler class, derived from CConnection.
This provides all the functionality for one Chat Server Connection.
GetReply and Sender alter the default behaviour from sending CStrings to ordinary strings.
class CChat : public CConnection { void Say (const CString& Msg) {((CChatServer*)GetServer())->Say(ID+Msg);} void Broadcast(const CString& Msg) {Say(Msg); ((CChatServer*)GetServer())->Broadcast(ID+Msg);} CString ID; CString GetReply() { CString S; try { ArI->ReadString(S); // It'll wait here for as long as the connection is OK. If the connection is broken it returns an empty string. }catch(...) {return '¿'+sConnectionBroken;} return S; } public: CChat(CSocketServer* Me) : CConnection(Me) {} protected: void Sender(const CString& Msg) {ArO->WriteString(Msg+"\r\n"); ArO->Flush();} void OnDisconnect() {Broadcast("Disconnected");} void OnConnect() { ID.Format("%s(%i): ", RemoteIP, m_hSocket); Broadcast("Connected"); Send("You will appear as "+ID); } void OnReceive() { CString S(GetReply()); if(*S=='¿') {Say(S.Mid(1)); return;} if(!S.CompareNoCase("Bye")) Disconnect(); Broadcast(S); } };Create the server in OnInitDialog, destroy it in the destructor and create a Button labelled [Broadcast] which sends some text in an Edit Box to all connections:
BOOL CServerDlg::OnInitDialog() { CDialog::OnInitDialog(); Chat=new CChatServer(23,this); // C:\Windows\System32\drivers\etc\SERVICES file lists Telnet as TCP port 23 return TRUE; // return TRUE unless you set the focus to a control } CServerDlg::~CServerDlg() {delete Chat;} void CServerDlg::OnBroadcast() { CString S; GetDlgItemText(IDC_EDIT1, S); Say("Broadcasted: "+S); Chat->Broadcast("Server: "+S); }The CConnection will make the CSocketServer hold a list of CChats.
If the Server didn't need to give any visual feedback (a if running as a Service for example)
you put the following in your Application class InitInstance():
#include "Telnet.h" BOOL CMyApp::InitInstance() { CSocketServer(23); // C:\Windows\System32\drivers\etc\SERVICES file lists Telnet as TCP port 23 ... }So you will now have an application which runs a Server listening to TCP port 23 which will create a new instance of CChat for each new Connection.
When this application is running, you can open Telnet: [Start Menu][Programs][Accessories][Telnet][File Menu][Remote System...][Host Name]127.0.0.1[Connect]
Then type Hi and press return, then bye and press return to watch the session work.
Incidentally, 127.0.0.1 is the IP Address used to refer to the local computer regardless of the IP Addresses of any network cards it may have.
CArchive and CString:
Usually you should be able to just use CStrings to hold data using the << and >> operators which send and use the data length before the string data.
You can't use CArchive::ReadString and CArchive::WriteString to transfer data because they scan the data looking for '\n' and decide that's the end of the string; the data length is not sent.
You can run into confusion when holding data in CStrings if you use any functions (like fputs) which look for a Null Terminator instead of using the data Length.
It is possible to use the CArchive::Write or CArchive::WriteString but you must be careful in the Server because data is sent in a queue and direct accesses like this won't be using the queue...
Send appends the message to the Message Queue.
The Message Queue doesn't get processed until you return from OnReceive().
In this example:
void OnReceive() { Send("Hello"); ArO << MyObject; } Hello will be sent _after_ MyObject! Use Sender("Hello") or ArO << CString("Hello") << MyObject; to send Messages with objects.You can download a complete Chat Server project at the top of the page that includes a Client although it works with Telnet as a client too:
(Known Bug: Client crashes if started with no Server available)
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.