Windows程序开发中,如果涉及到网络编程的话,一般少不了socket,socket作为应用层与传输层之间的一个抽象层,可以理解为应用程序与网络协议之间的编程接口。

我曾通过MFC开发了一个简单C/S(客户端/服务器)模式的应用程序,主要是为了实现手机控制电脑,android网络编程也可以通过socket实现,这里以PC端编写服务器程序和客户端程序为例说明socket编程的一般步骤。

工具/原料

  • Visual Studio 2010/2013

创建项目

  1. 1

    首先创建一个MFC项目,修改名称以及存放路径。

  2. 2

    项目配置,在向导过程中选择“基于对话框”模式,并选择“windows”套接字。

  3. 3

    设计服务器界面,控件有:4个静态文本(Static Text),最后一个用于指示用户连接个数;一个按钮(Button),用于打开或关闭服务器;2个编辑框(Edit Control),一个用于输入端口号,另一个只读的用于显示事件日志。

  4. 4

    修改各个控件的属性,注意编辑框2还要把Multiline和Vertical Scroll属性选为true,以实现多行显示并自带滚动条。

  5. 5

    1、 给控件添加变量和事件处理函数,这个通过类向导就可以完成,变量如上表所示,事件只有“按钮按下”一个,双击按钮自动生成函数,后面添加相关代码即可。

    2、 在类视图中添加一个新类CServerSocket,派生于CSocket类,对该类进行类向导添加三个函数:

    OnAccept()、OnClose()、OnReceive()

    END

编写服务器类

  1. 1

    看看CServerSocket的类视图,需要修改的有之前生成的三个函数。

  2. 2

    修改头文件ServerSocket.h,定义主对话框的指针变量

    #pragma once

    #include "PhoneServerDlg.h"                              // 主对话框头文件

    class CPhoneServerDlg;                                      //别忘了加上   

    class CServerSocket : public CSocket

    {

    public:

             CPhoneServerDlg* m_pDlg;                       // 主对话框指针对象

             CServerSocket();

             virtual ~CServerSocket();

             virtual void OnReceive(int nErrorCode);

             virtual void OnClose(int nErrorCode);

             virtual void OnAccept(int nErrorCode);

    };

  3. 3

    修改源文件ServerSocket.cpp,注意其中调用的函数都在主对话框类中定义。

    void CServerSocket::OnReceive(int nErrorCode)

    {

             m_pDlg->RecvData(this);                           // 接收数据

             CSocket::OnReceive(nErrorCode);

    }

    void CServerSocket::OnClose(int nErrorCode)

    {

             m_pDlg->RemoveClient(this);                   // 删除下线用户

             CSocket::OnClose(nErrorCode);

    }

    void CServerSocket::OnAccept(int nErrorCode)

    {

             m_pDlg->AddClient();                                  //添加上线用户

             CSocket::OnAccept(nErrorCode);

    }

    至此,CServerSocket类的代码就完成了,接下来编写主类相关函数。

    END

编写主对话框类

  1. 1

    修改头文件PhoneServerDlg.h 

    1、   添加服务器类的头文件。

    #include "ServerSocket.h"

    class CServerSocket;                  //一定要加上

    2、   添加函数声明和变量定义

    CServerSocket* listenSocket;     // 用于打开服务器

    CPtrList m_clientList;                 // 链表用于存储用户

    bool m_connect;                       // 用于标记服务器状态

    void AddClient();                       // 增加用户,响应用户请求

    void RemoveClient(CServerSocket* pSocket);          // 移除下线的用户

    void RecvData(CServerSocket* pSocket);                 // 获取数据

    void UpdateEvent(CString str);  // 更新事件日志

    BOOL WChar2MByte(LPCWSTR srcBuff, LPSTR destBuff, int nlen);        

    //字符转换

    void SendMSG(CString str);       // 发送消息给各个客户端

    void ControlPC(CString AndroidControl);  // 手机控制PC的响应函数

  2. 2

    修改PhoneServerDlg.cpp实现头文件中声明的函数

    1、 首先实现“打开服务器”按钮的响应函数

    void CPhoneServerDlg::OnBnClickedStartserver()

    {

             // TODO:  在此添加控件通知处理程序代码

             if (m_connect)

             {

                       delete listenSocket;

                       listenSocket = NULL;

                       m_connect = false;

                       SetDlgItemText(IDC_StartServer, _T("打开服务器"));

                       UpdateEvent(_T("系统关闭服务器."));

                       return;

             }

             listenSocket = new CServerSocket();

             listenSocket->m_pDlg = this;

    // 指定对话框为主对话框,不能少了这句

             UpdateData(true);

             if (!listenSocket->Create(m_port))                     

    // 创建服务器的套接字,IP地址默认本机IP

             {

                       AfxMessageBox(_T("创建套接字错误!"));

                       listenSocket->Close();

                       return;

             }

             if (!listenSocket->Listen())

             {

                       AfxMessageBox(_T("监听失败!"));

                       listenSocket->Close();

                       return;

             }

             m_connect = true;

             SetDlgItemText(IDC_StartServer, _T("关闭服务器"));

             UpdateEvent(_T("系统打开服务器."));

    }

    说明:本函数用于打开或关闭服务器,主要用到Create函数和Listen函数用于创建服务器和监听客户端。其中端口号从编辑框获取,应用程序的可用端口范围是1024-65535。

  3. 3

    2、 编写AddClient函数,用于增加用户,响应用户请求

    void CPhoneServerDlg::AddClient()

    {

             CServerSocket *pSocket = new CServerSocket;

             pSocket->m_pDlg = this;

             listenSocket->Accept(*pSocket);

             pSocket->AsyncSelect(FD_READ | FD_WRITE | FD_CLOSE);

             m_clientList.AddTail(pSocket);

             m_userCount = m_clientList.GetCount();

             UpdateData(false);

             UpdateEvent(_T("用户连接服务器."));

             SendMSG(_T("Hello!"));

    }

    说明:本函数在CServerSocket类中的OnAccept消息中调用,用于响应用户连接服务器的请求,主要函数为Accept,当连接成功后,通过链表m_clientList保存新用户,更新日志,向新用户发送“Hello”表示欢迎。

  4. 4

    3、 编写RemoveClient函数,用于移除下线的用户

    void CPhoneServerDlg::RemoveClient(CServerSocket* pSocket)

    {

             POSITION nPos = m_clientList.GetHeadPosition();

             POSITION nTmpPos = nPos;

             while (nPos)

             {

                       CServerSocket* pSockItem = (CServerSocket*)m_clientList.GetNext(nPos);

                       if (pSockItem->m_hSocket == pSocket->m_hSocket)

                       {                          

                                pSockItem->Close();

                                delete pSockItem;

                                m_clientList.RemoveAt(nTmpPos);

                                m_userCount = m_clientList.GetCount();

                                UpdateData(false);

                                UpdateEvent(_T("用户离开."));

                                return;

                       }

                       nTmpPos = nPos;

             }

    }

    说明:本函数在CServerSocket类中的OnClose消息中调用,用到POSITION结构,查找存储用户中哪位用户下线了,将下线用户释放,从链表中删除,并更新日志。

  5. 5

    4、  编写RecvData函数,用于获取数据

    void CPhoneServerDlg::RecvData(CServerSocket* pSocket)

    {

             char* pData = NULL;

             pData = new char[1024];

             memset(pData, 0, sizeof(char)* 1024);

             UCHAR leng = 0;

             CString str;

             if (pSocket->Receive(pData, 1024, 0) != SOCKET_ERROR)

             {

                       str = pData;

                       ControlPC(str);     // 依据指令控制电脑

                       SendMSG(str);     // 转发数据给所有用户,包括发送数据的用户

             }

             delete pData;

             pData = NULL;

    }

    说明:本函数在CServerSocket类中的OnReceive消息中调用,用于处理接收到的数据并控制电脑,并将数据转发给所有用户(类似于群消息),通过CSocket类的GetPeerName函数可以获取用户的IP和端口号。

  6. 6

    5、  编写UpdateEvent函数,用于更新事件日志

    void CPhoneServerDlg::UpdateEvent(CString str)

    {

             CString string;

             CTime time = CTime::GetCurrentTime();                  

    // 获取系统当前时间

             str += _T("\r\n");                                                             

    // 用于换行显示日志

             string = time.Format(_T("%Y/%m/%d %H:%M:%S  ")) + str;         

    // 格式化当前时间

             int lastLine = m_event.LineIndex(m_event.GetLineCount() - 1);

    //获取编辑框最后一行索引

             m_event.SetSel(lastLine+1,lastLine+2,0);                                     

    //选择编辑框最后一行

             m_event.ReplaceSel(string);                                                             //替换所选那一行的内容

    }

    说明:本函数在所有需要更新日志的地方都有调用,方便服务器记录用户的登录和退出事件。

  7. 7

    6、  编写WChar2MByte函数,用于实现字符转换

    BOOL CPhoneServerDlg::WChar2MByte(LPCWSTR srcBuff, LPSTR destBuff, int nlen)

    {

             int n = 0;

             n = WideCharToMultiByte(CP_OEMCP, 0, srcBuff, -1, destBuff, 0, 0, FALSE);

             if (n<nlen)return FALSE;

             WideCharToMultiByte(CP_OEMCP, 0, srcBuff, -1, destBuff, nlen, 0, FALSE);

             return TRUE;

    }

    说明:本函数在发送函数SendMSG中调用,用于字符集的转换,将宽字符转换为多字符集,不经转换的话,接收方只能接收一个字节。

  8. 8

    7、  编写SendMSG函数,用于发送消息给各个客户端

    void CPhoneServerDlg::SendMSG(CString str)

    {

             char *pSend = new char[str.GetLength()];

             memset(pSend, 0, str.GetLength()*sizeof(char));

             if (!WChar2MByte(str.GetBuffer(0), pSend, str.GetLength()))

             {

                       AfxMessageBox(_T("字符转换失败"));

                       delete pSend;

                       return;

             }

             POSITION nPos = m_clientList.GetHeadPosition();

             while (nPos)

             {

                       CServerSocket* pTemp = (CServerSocket*)m_clientList.GetNext(nPos);

                       pTemp->Send(pSend, str.GetLength());

             }

             delete pSend;

    }

    说明:发送函数,用于发送消息给所有用户,主要函数为Send,在AddClient和RecvData中都有调用,可以随时调用发消息给用户。

  9. 9

    8、 编写ControlPC函数,用于处理接收到的指令并控制电脑,主要是为了实现手机控制而写。

    void CPhoneServerDlg::ControlPC(CString AndroidControl)

    {

             if (AndroidControl == "mop")             //打开播放器

             {

             ShellExecute(NULL, _T("open"), _T("C:\\Program Files (x86)\\KuGou\\KGMusic\\KuGou.exe"), NULL, NULL, SW_SHOWNORMAL);

             }

             else if (AndroidControl == "mcl")         //关闭播放器

             {

                       DWORD id_num;

                       HWND hWnd = ::FindWindow(_T("kugou_ui"), NULL);

                       GetWindowThreadProcessId(hWnd, &id_num);                      

    //注意:第二个参数是进程的ID,返回值是线程的ID。

                       HANDLE hd = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id_num);

                       TerminateProcess(hd, 0);

             }

             else if (AndroidControl == "mpl" || AndroidControl == "mpa")                //播放/暂停

             {

                       keybd_event(VK_LMENU, 0, 0, 0);

                       keybd_event(VK_F5, 0, 0, 0);

                       keybd_event(VK_F5, 0, KEYEVENTF_KEYUP, 0);

                       keybd_event(VK_LMENU, 0, KEYEVENTF_KEYUP, 0);

             }

    }

    说明:控制功能可以自己随意添加,这里只以音乐播放为例进行说明,ShellExecute函数用于调用其他应用程序,关闭进程比较麻烦一点,这里先获取应用程序窗口的ID,通过OpenProcess和TerminateProcess终止进程。

  10. 10

    9、 类向导添加虚函数PreTranslateMessage,并编写代码

    BOOL CPhoneServerDlg::PreTranslateMessage(MSG* pMsg)

    {

             switch (pMsg->wParam)

             {

             case VK_RETURN:

             case VK_ESCAPE:

                       return true; break;

             }

             return CDialogEx::PreTranslateMessage(pMsg);

    }

    说明:该函数用于防止按下enter或者esc时退出程序。

  11. 11

    经过以上步骤,服务器端的程序就完成了,虽然函数有点多,但只要理解了socket的使用流程并不难,总体可以概括为4步:

    1、创建服务器

    2、连接请求连接的客户端

    3、与客户端进行数据传输(发送和接收)

    4、客户端断开服务器

    加上一些辅助代码,可以更好的实现网络通讯。客户端的程序会在下一篇经验中介绍。

    END

注意事项

  • 注意类与类之间头文件的引用
  • 如果不重写虚函数PreTranslateMessage会导致按下回车或者esc时自动退出程序
  • 合理设置端口号,不要与其他程序发生冲突