开发文章

基于TCP/IP的SOCKET客户端与服务端多用户通信程序编写

摘要:基于TCP/IP的网络通信技术实现了面向连接的用户与服务器间点对点异步通信,本文在该基础上应用了多线程以及共享数据结构技术,使网络服务器具有了多用户间数据转发的功能,进而解决了局域网多用户间的通信问题。


引言

由于因特网的迅速流行,越来越多的应用程序具备了在网上与其它程序通信的能力。从WIN95开始微软把网络功能融进了它的操作系统,使得应用程序网络通信能力更为普及。因此,微软的TCP/IP协议也就成为网络应用程序基于的首选协议。

一般采用TCP/IP协议的应用程序只实现了单用户与服务器间点对点的连接,而本文在VC6.0的环境下,运用了了多线程以及共享数据结构技术,不仅实现了多用户与服务器间的连接,而且解决了多用户间信息互发问题----依靠服务器的转发功能。通过本文的阐述,希望能对那些需要编写多用户网络通信程序的读者以启发。

 一、技术概述
1.1 基于TCP/IP的通信技术

基于TCP/IP的通信基本上都是利用SOCKET套接字进行数据通讯,程序一般分为服务器端和用户端两部分。下面简要地讲一下设计思路(VC6.0下):

第一部分 服务器端
  一、创建服务器套接字(create)。
  二、服务器套接字进行信息绑定(bind),并开始监听连接(listen)。
  三、接受来自用户端的连接请求(accept)。
  四、开始数据传输(send/receive)。
  五、关闭套接字(closesocket)。

第二部分 用户端
  一、创建用户套接字(create)。
  二、与远程服务器进行连接(connect),如被接受则创建接收进程。
  三、开始数据传输(send/receive)。
  四、关闭套接字(closesocket)。

通过以上设计思路,我们可以创建一个简单的面向连接的单用户程序。下面,将介绍多线程技术,以使程序支持多用户。

1.2 多线程技术

我们可以把线程看成是一个进程(执行程序)中的一个执行点,每个进程在任何给定时刻可能有若干个线程在运行。一个进程中的所有线程共享该进程中同样的地址空间,同样的数据和代码,以及同样的资源。进程中每个线程都有自己独立的栈空间,和其它线程分离,并且不可互相访问。每个线程在本进程所占的CPU时间内,要么以时间片轮换方式,要么以优先级方式运行。如果以时间片轮换方式运行,则每个线程获得同样的时间量;如果以优先级方式运行,则优先级高的线程将得到较多的时间量,而优先级低的线程只能得到较少的时间量。方式的选择主要取决于系统时间调度器的机制以及程序的实时性要求。

现在,运用多线程技术就可以实现对多用户的支持。即在服务器端,使接收来自用户端的连接请求(accept)这步无限循环,每接收一个用户请求,产生两个线程(send和receive线程),用来管理服务器与该用户的通信任务。下面,运用共享数据结构技术,就可以实现本问所要解决的关键技术---服务器转发技术。

1.3 共享数据结构技术

同一进程中的多个线程共存于单一的线性地址空间,因此,在多线程间共享数据结构是非常容易且方便的。但必须注意的是,对数据结构的访问必须是多线程互斥的,否则数据任意更改将导致不可预料的结果。本文所阐述的服务器转发技术也就是通过共享数据结构实现线程间的互相通信。

 二、实现方案

整体方案的构思图如下:

基于TCP/IP的SOCKET客户端与服务端多用户通信程序编写

 通过上图,我们可以看到整个系统分为三个相关的程序,即注册/登陆服务器、通信服务器以及用户程序。其中,注册/登陆服务器负责用户的注册、登陆以及数据库管理;通信服务器负责完成数据转发以及共享数据结构的管理;用户端则完成注册、登陆和通信功能。为什么要把服务器分为两部分呢?主要是考虑到服务器的用户容量问题,以及对通信服务器的保护,只有在通过验证后,用户在能与通信服务器连接。

由此可见,整个系统通信任务的实现还是很复杂的。用户端首先必须注册自己,等待注册成功;然后根据自己的注册信息进行服务器登陆,登陆成功后才能与通信服务器连接,进行用户间通信。

注册/登陆服务器接收到用户端的信息后,首先判断是注册信息还是登陆信息。如果是注册信息,则将该数据按预定的格式写入数据库,然后返回注册成功的消息,期间有任何异常产生,服务器都会返回注册失败消息,提示用户重新注册;如果是登陆信息,则从数据中提取用户名和ID与数据库中的内容进行比较,如果该用户存在,则返回登陆成功消息,反之,返回登陆失败消息。

通信服务器所完成的主要功能是数据转发,这是通过与图中的共享数据结构进行交互完成的。服务器接收到用户端发来的消息后,提取消息的一部分与共享数据结构存储的内容进行比较,确定所要转发的对象,最后通过多线程及其通信机制完成数据转发。 下面,我们将分三部分来讨论系统的具体实现过程。

 

 三、具体实施
3.1 注册/登陆服务器

注册/登陆服务器程序是基于对话框的,该程序使用I/O端口56789与用户端连接。
首先,在对话框初始化的同时完成网络初始化,即执行Init_net()函数,代码(不完整)如下:

复制内容到剪贴板
  1. BOOL CServerDlg::Init_net()   
  2. {////////////////////////网络初始化///////////////////////////////   
  3.     addrLen=sizeof(SOCKADDR_IN);   
  4.     status=WSAStartup(MAKEWORD(1, 1), &Data);   
  5.     ………   
  6.     memset(&serverSockAddr, 0, sizeof(serverSockAddr));   
  7.   
  8. /*以下指定一个与某个SOCKET连接本地或远程地址*/  
  9.   
  10.     serverSockAddr.sin_port=htons(PORT);   
  11.     serverSockAddr.sin_family=AF_INET;   
  12.     serverSockAddr.sin_addr.s_addr=htonl(INADDR_ANY);   
  13.     serverSocket=socket(AF_INET, SOCK_STREAM, 0);//初始化SOCKET   
  14.     ………   
  15.   
  16.     status=bind(serverSocket,(LPSOCKADDR)&serverSockAddr,sizeof(serverSockAddr)); //将SOCKET与地址绑定   
  17.     ………   
  18.     status=listen(serverSocket, 5); //开始监听   
  19.     ………   
  20.     return true;   
  21. }  

接着按下RUN键开始服务器功能,执行Reg_Load()函数,使服务器始终处于等待连接状态,但这样也使该线程始终阻塞。当有用户连接时,该函数创建一个任务用于处理与用户及数据库的事务。具体任务函数略(详见原始代码文件)。

复制内容到剪贴板
  1. void CServerDlg::Reg_Load()   
  2. {      
  3.     while(1)   
  4.     {   
  5.         CWinThread*  hHandle;   
  6.         clientSocket=accept(serverSocket,(LPSOCKADDR)&clientSockAddr,&addrLen); //等待连接,阻塞   
  7.         hHandle=AfxBeginThread(talkToClient,(LPVOID)clientSocket);//有连接时,创建任务   
  8.             ………   
  9.     }   
  10. }  

任务函数在接收到消息时,要对数据库进行操作,由于数据库较简单,采用ODBC连接ACCESS数据库(将netuser.mdb在ODBC数据管理器中安装成同名数据源)具体代码略。
3.2 通信服务器

通信服务器是本程序实现的关键,它运用共享数据结构技术及多线程技术,通过I/O端口56790与用户端连接,实现了数据转发功能。首先,程序初始化网络Init_net(),接着当用户连接到服务器时,创建接收线程和发送线程,这样就可以实现数据转发。最后,当用户断开连接时,服务器关闭与他的连接,并结束相应的线程。

下面我们来看一下本程序中的共享数据结构的具体内容与使用方法以及多线程的相关内容与实现。

● 共享数据结构

本程序的共享数据结构一共有两个,即socket_info和send_info。前者包含了所有登陆用户的一些基本资料,后者则包含了当前服务器接收到的用户端所发送的信息资料。详细内容及注释如下:

复制内容到剪贴板
  1. struct socket_info     
  2. {   
  3.     SOCKET s_client;                    //用户的SOCKET值   
  4.     u_long client_addr;             //用户网络地址   
  5.     CString pet;                        //用户昵称   
  6.     CWinThread* thread;             //为该用户创建的发送线程对象的指针   
  7. };   
  8.   
  9. struct send_info   
  10. {   
  11.     CString data;                   //用户端发送的数据内容(经过编辑)   
  12.     CWinThread* thread;             //需要发送数据的任务指针   
  13. };  

在程序中,定义两个全局变量,用来在线程间共享: 

复制内容到剪贴板
  1. send_info info_data; CList<socket_info,socket_info&>s_info;  

每当有用户连接到服务器,服务器就将用户端的一些信息以socket_info结构体的形式存入s_info列表中;而当服务器接收到用户端发送过来的数据时,就将数据格式化后存入结构体info_data,通过与结构体列表比较,确定需要恢复的发送线程(所有发送线程在创建时都被挂起)。这样,服务器就准确地转了发数据。

●多线程
每当服务器上有用户连接成功,服务器都会为其创建两个线程:接收线程(RecvData)和发送线程(SendData),并且接收线程在创建后处于可执行状态,而发送线程则阻塞,等待服务器将其唤醒。这两个线程都执行一个无限循环的过程,只有当通信出现异常或用户端关闭连接时,线程才被自身所结束,并且,这两个线程一定是同时生成,同时结束的。很显然,每个连接产生两个线程,使得数据转发变的简单,但同时又使得服务器的任务加重。因此,用户端的连接数量有所限制,视服务器软、硬件能力而定。

同时,由于多线程对结构体info_data都需要操作,所以线程间必须同步。这儿,我定义了互斥量CMutex m_mutex,用它的方法Lock()和Unlock()来完成同步。

我们首先来看一下接收线程(RecvData):(不完整代码)

复制内容到剪贴板
  1. UINT RecvData(void* cs)   
  2. {      
  3.     SOCKET clientSocket=(SOCKET)cs;   
  4.     while(1)   
  5.     {   
  6.         numrcv=recv(clientSocket, buffer, MAXBUFLEN, NO_FLAGS_SET);   
  7.         buffer[numrcv]=''\0'';   
  8.         if(strcmp(buffer,"Close!")!=0)  //不是接收的“Close”数据   
  9.         {   
  10.             …………   
  11.                 for(i=0;i<count;i++)   
  12.                 {   
  13.                 if(po!=NULL)   
  14.                 {   
  15.                     s1=s_info.GetNext(po);   
  16.                     if(s1.pet.Compare(petname)==0)      //比较昵称是否一样   
  17.                     {   
  18.                         m_mutex.Lock();    //互锁   
  19.                         info_data.data=pos;   
  20.                         info_data.thread=s1.thread;   
  21.                         m_mutex.Unlock();  //解锁   
  22.                     }   
  23.                     s1.thread->ResumeThread(); //恢复发送相应的线程   
  24.                     break;   
  25.                 }   
  26.             }   
  27.         }   
  28.         else  
  29.         {   
  30.             …………   
  31.             if(clientSocket==s1.s_client)   
  32.             {   
  33.                 m_mutex.Lock(); //互锁   
  34.                 info_data.data=buffer;   
  35.                 m_mutex.Unlock();           //解锁   
  36.                 s1.thread->ResumeThread();  //恢复发送相应的线程   
  37.                 s_info.RemoveAt(po1);       //删除该用户信息   
  38.                 break;   
  39.             }   
  40.             ………   
  41.             goto aa;   
  42.         }   
  43.     }   
  44.     aa: closesocketlink((LPVOID)clientSocket);          //关闭连接   
  45.     AfxEndThread(0,true);                           //结束本线程   
  46.     return 1;   
  47. }  

接下来看一下发送线程(SendData):(不完整代码)
 

复制内容到剪贴板
  1. UINT SendData(void* cs)   
  2. {   
  3.     SOCKET clientSocket=(SOCKET)cs;   
  4.     while(1)   
  5.     {   
  6.         if(info_data.data!="Close!")   
  7.         {   
  8.             m_mutex.Lock();               //互锁   
  9.             numsnd=send(clientSocket,info_data.data,   
  10.             info_data.data.GetLength(),NO_FLAGS_SET); //发送数据   
  11.             now=info_data.thread;   
  12.             m_mutex.Unlock();             //解锁   
  13.             now->SuspendThread();         //自身挂起   
  14.         }   
  15.         else  
  16.         {   goto bb; }   
  17.   
  18.     }   
  19.     bb: closesocketlink((LPVOID)clientSocket);    //关闭连接   
  20.     AfxEndThread(0,true);                         //结束本线程   
  21.     return 1;   
  22. }  

3.3 用户端
很显然,用户端不用考虑多线程,网络连接技术也比较成熟,因此在通信方面没有什么难题。但是,用户端是面向实际用户的,所以,不论是界面还是功能都必须友好。就像大多数软件的更新一样,界面友好度的提高以及功能的完善往往是放在首位的。由此可见,单从总体设计与技术实现角度来讲,用户端的工作量是十分大的,并且设计较服务器端复杂得 多。我粗略总结了以下几条:

●与服务器通信格式兼容;

●操作简单、易用,有美观的界面及快捷键;

●准确地接收和传输数据;

●所有的数据记录与提取功能;

●多种消息接收提示方式,比如托盘图标(发送者头像)闪烁、声音提示等;

根据以上内容,我设计了三个独立的对话框分别用来完成注册、登陆、通信功能,登陆和注册对话框与服务器的56789I/O端口连接,通信对话框与服务器的56790I/O端口连接,这样就很好地实现了注册登陆与通信的隔离,既能使服务器负载降低,同时又能保证一定的通信安全性。

由于本部分不是主要内容,详细代码见程序。

 四、结束语

通过以上阐述可以知道,本系统分为服务器端和用户端,服务器端又分为注册/登陆服务器和通信服务器,通过通信服务器的转发功能实现了局域网内的多用户通信功能。本文运用了多线程技术和共享数据结构技术实现了通信服务器的转发功能,使一般基于TCP/IP的网络应用程序得到了发展。本系统已经在我实验室的局域网(一台服务器,二十台客户机)运行通过。

 本文示例代码

http://www.panshy.com/downloads/201505/code-268.html

参考文献:

[1] Eugene Olafsen ,Kenn Scribner, K.David White等著. MFC Visual C++ 6.0编程技术内幕. 北京:机械工业出版社 2000.2
[2] Charles Wright. Visual C++程序员实用大全. 北京:中国水利水电出版社 2001.10



 

感谢 袁 渊 支持 磐实编程网 原文地址:
袁 渊

文章信息

发布时间:2015-05-17

作者:袁 渊

发布者:aquwcw

浏览次数: