VISUAL C++ 网络与通讯

来源:互联网 发布:国内erp软件排名 编辑:程序博客网 时间:2024/04/29 21:00
网络篇
POP3协议封装类
目前互连网上,EMAIL的使用是越来越广泛了。在所有的TCP连接线路中,大概有一半的线路是用来收发EMAIL的。因此,许多网络应用程序要想离开POP3协议是不可能的。 

  而且,很多EMAIL系统都是使用SMTP协议来作为发送协议,而POP3协议来作为接受协议。关于这2个协议的许多资料,你可以看一下RCF821(关于SMTP的)和RCF1225(关于POP3的)。虽然在WINDOWS操作系统中,有一个叫MAPI应用程序是来管理EMAIL的,但是它的功能还不够强大,不足以直接处理一些EMAIL服务。所以,我写了一个有关于POP3协议的C++类,在这个类中我还使用了CSocket作为一个类成员(这好象有点不可想象),这样的话呢,我们可以在连接的时候使用它。并且,该POP类使用的一些功能类似于POP3协议中的一些命令。下面就代码: 

/*-------------------------------------------------------------------- 
Pop.h : main header file for the POP application 
-----------------------------------------------------------------------*/ 

#if !defined(AFX_POP_H__A44B38B6_697C_11D1_881E_00001C302581__INCLUDED_) 
#define AFX_POP_H__A44B38B6_697C_11D1_881E_00001C302581__INCLUDED_ 

#define CONNECTION_CHECK 0 
#define USER_CHECK 1 
#define PASSWORD_CHECK 2 
#define QUIT_CHECK 3 
#define DELETE_CHECK 4 
#define RSET_CHECK 5 
#define STAT_CHECK 6 
#define NOOP_CHECK 7 
#define LIST_CHECK 8 
#define RETR_CHECK 9 

///////////////////////////////////////////////////////////////////////////// 
class CPop 

public: 
BOOL List(); 
CWordArray m_SizeOfMsg; 
CString GetErrorMessage(); // If there is any error this will return it method 
CString GetPassword(); // Getting Password stored in class 
void SetPassword(CString& Password); // Setting Password in class 
CString GetUser(); // Getting user name stored in class 
void SetUser(CString& User); // Setting user name in class 
CString GetHost(); // Getting Host name (email server name) stored in class
void SetHost(CString& Host); // Setting Host name (email server name) in class 
BOOL Connect(); // Connecting to email server 
int GetTotalMailSize(); // it returns the Total Mail Size 
int GetNumberOfMails(); // It return the number of mails 
CString GetMsgContents(); 
BOOL Statistics(); // issue the STAT command on email server 
BOOL Retrieve(int MsgNumber); // Getting any particular mail message 
BOOL Reset(); // issue the reset command on email server 
int GetMessageSize(int MsgNumber); // Return a size of any particular mail 
BOOL Noop(); // issue the NOOP command on email server 
BOOL Disconnect(); // issue the QUIT command on email server 
BOOL Delete(int& MsgNumber); // Deleteing a particular message from email server 
BOOL Connect(CString& Host, CString& User, CString& Password); 
CPop(); 
virtual ~CPop(); 

private: 
CString m_ErrorMessage; 
BOOL CheckResponse(int ResponseType); 
CString m_Password; 
CString m_User; 
CString m_Host; 
CString m_MsgContents; 
int m_TotalSize; 
int m_NumberMail; 
CSocket m_PopServer; 
}; 
/#endif // !defined(AFX_POP_H__A44B38B6_697C_11D1_881E_00001C302581__INCLUDED_) 


/*----------------------------------------------------------------------------------------------- 
// Pop.cpp : Defines the class behaviors for the application. 
---------------------------------------------------------------------------------------------------*/ 

#include "stdafx.h" 
#include "Pop.h" 

#ifdef _DEBUG 
#define new DEBUG_NEW 
#undef THIS_FILE 
static char THIS_FILE[] = __FILE__; 
#endif 

//////////////////////////////////////////////////////////////////// 
// CPop Class 
////////////////////////////////////////////////////////////////////// 

////////////////////////////////////////////////////////////////////// 
// Construction/Destruction 
////////////////////////////////////////////////////////////////////// 

CPop::CPop() 

m_PopServer.Create(); 

//{4EEC1C91-6BE1-11d1-8824-00001C302581} 

CPop::~CPop() 

m_PopServer.Close(); 


BOOL CPop::Connect(CString & Host, CString & User, CString & Password) 

char buf [512]; 

if (!m_PopServer.Connect(Host,110)) // 110 Pop3 Port 

m_ErrorMessage = _T("Server cannot be connected"); 
return FALSE; 

else 

if(CheckResponse(CONNECTION_CHECK)==FALSE) 
return FALSE; 

wsprintf (buf, "USER %s\r\n", (LPCSTR) User); 
m_PopServer.Send(buf, strlen (buf)); 
if(CheckResponse(USER_CHECK)==FALSE) 
return FALSE; 

wsprintf (buf, "PASS %s\r\n", (LPCSTR) Password); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(PASSWORD_CHECK)==FALSE) 
return FALSE; 

return TRUE; 




BOOL CPop::Delete(int & MsgNumber) 

char buf [512]; 

wsprintf (buf, "DELE %d\r\n",MsgNumber ); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(DELETE_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


BOOL CPop::Disconnect() 

char buf [512]; 

wsprintf (buf, "QUIT \r\n"); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(QUIT_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


BOOL CPop::Noop() 

char buf [512]; 

wsprintf (buf, "NOOP \r\n"); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(NOOP_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


// Return the Msg Size for given msg number 
int CPop::GetMessageSize(int MsgNumber) 

if(m_SizeOfMsg.GetSize() < MsgNumber+1) 
return 0; 
else 
return m_SizeOfMsg[MsgNumber+1]; 


BOOL CPop::Reset() 

char buf [512]; 

wsprintf (buf, "RSET \r\n"); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(RSET_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


// MsgContents will hold the msg body 
BOOL CPop::Retrieve(int MsgNumber) 

char buf [512]; 

wsprintf (buf, "RETR %d\r\n",MsgNumber ); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(RETR_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


BOOL CPop::Statistics() 

char buf [512]; 

wsprintf (buf, "STAT \r\n"); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(STAT_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 


CString CPop::GetMsgContents() 

return m_MsgContents; 


int CPop::GetNumberOfMails() 

return m_NumberMail; 


int CPop::GetTotalMailSize() 

return m_TotalSize; 


BOOL CPop::Connect() 

Connect(m_Host, m_User, m_Password); 
return TRUE; 


void CPop::SetHost(CString & Host) 

m_Host = Host; 


CString CPop::GetHost() 

return m_Host; 


void CPop::SetUser(CString & User) 

m_User = User; 


CString CPop::GetUser() 

return m_User; 


void CPop::SetPassword(CString & Password) 

m_Password = Password; 


CString CPop::GetPassword() 

return m_Password; 


BOOL CPop::CheckResponse(int ResponseType) 

char buf [1000]; 

for (int i=0;i<512;i++) 
buf[i]='\0'; 

m_PopServer.Receive(buf, sizeof(buf)); 

switch (ResponseType) 

case CONNECTION_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Bad Connection"); 
return FALSE; 

break; 

case USER_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Bad User Name"); 
return FALSE; 

break; 
case PASSWORD_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Bad Password Name"); 
return FALSE; 

break; 
case QUIT_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during QUIT"); 
return FALSE; 

break; 
case DELETE_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during DELE"); 
return FALSE; 

break; 
case RSET_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during RSET"); 
return FALSE; 

break; 
case STAT_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during STAT"); 
return FALSE; 

else 

BOOL EmailNumber = TRUE; 
for (char *p = buf; *p != '\0'; p++) 

if (*p == '\t' || *p == ' ') 

if(EmailNumber == TRUE) 

m_NumberMail = atoi(p); 
EmailNumber = FALSE; 

else 

m_TotalSize = atoi(p); 
return TRUE; 





break; 
case NOOP_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during NOOP"); 
return FALSE; 

break; 

case LIST_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during LIST"); 
return FALSE; 

else 

m_PopServer.Receive(buf, sizeof(buf)); 

for (char *p = buf; *p != '.'; p++) 
if (*p == '\t' || *p == ' ') 
m_SizeOfMsg.Add(atoi(p)); 

break; 
case RETR_CHECK: 
if (strnicmp(buf,"-ERR", 4) == 0) 

m_ErrorMessage = _T("Error occured during RETR"); 
return FALSE; 

else 

char temp[9000]; 
m_PopServer.Receive(temp, sizeof(temp)); 
m_MsgContents = temp; 

break; 

return TRUE; 


CString CPop::GetErrorMessage() 

return m_ErrorMessage; 


BOOL CPop::List() 

char buf [512]; 

wsprintf (buf, "LIST \r\n"); 
m_PopServer.Send(buf, strlen (buf)); 
if (CheckResponse(LIST_CHECK)==FALSE) 
return FALSE; 
else 
return TRUE; 
}
用VC++6.0的Sockets API实现一个聊天室程序

1.VC++网络编程及Windows Sockets API简介

  VC++对网络编程的支持有socket支持,WinInet支持,MAPI和ISAPI支持等。其中,Windows Sockets API是TCP/IP网络环境里,也是Internet上进行开发最为通用的API。最早美国加州大学Berkeley分校在UNIX下为TCP/IP协议开发了一个API,这个API就是著名的Berkeley Socket接口(套接字)。在桌面操作系统进入Windows时代后,仍然继承了Socket方法。在TCP/IP网络通信环境下,Socket数据传输是一种特殊的I/O,它也相当于一种文件描述符,具有一个类似于打开文件的函数调用-socket()。可以这样理解:Socket实际上是一个通信端点,通过它,用户的Socket程序可以通过网络和其他的Socket应用程序通信。Socket存在于一个"通信域"(为描述一般的线程如何通过Socket进行通信而引入的一种抽象概念)里,并且与另一个域的Socket交换数据。Socket有三类。第一种是SOCK_STREAM(流式),提供面向连接的可靠的通信服务,比如telnet,http。第二种是SOCK_DGRAM(数据报),提供无连接不可靠的通信,比如UDP。第三种是SOCK_RAW(原始),主要用于协议的开发和测试,支持通信底层操作,比如对IP和ICMP的直接访问。

  2.Windows Socket机制分析

  2.1一些基本的Socket系统调用

  主要的系统调用包括:socket()-创建Socket;bind()-将创建的Socket与本地端口绑定;connect()与accept()-建立Socket连接;listen()-服务器监听是否有连接请求;send()-数据的可控缓冲发送;recv()-可控缓冲接收;closesocket()-关闭Socket。

  2.2Windows Socket的启动与终止

  启动函数WSAStartup()建立与Windows Sockets DLL的连接,终止函数WSAClearup()终止使用该DLL,这两个函数必须成对使用。

  2.3异步选择机制

  Windows是一个非抢占式的操作系统,而不采取UNIX的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理,WSAAsyncSelect()函数就是用来选择系统所要处理的相应事件。当Socket收到设定的网络事件中的一个时,会给程序窗口一个消息,这个消息里会指定产生网络事件的Socket,发生的事件类型和错误码。

  2.4异步数据传输机制

  WSAAsyncSelect()设定了Socket上的须响应通信事件后,每发生一个这样的事件就会产生一个WM_SOCKET消息传给窗口。而在窗口的回调函数中就应该添加相应的数据传输处理代码。

  3.聊天室程序的设计说明

  3.1实现思想

  在Internet上的聊天室程序一般都是以服务器提供服务端连接响应,使用者通过客户端程序登录到服务器,就可以与登录在同一服务器上的用户交谈,这是一个面向连接的通信过程。因此,程序要在TCP/IP环境下,实现服务器端和客户端两部分程序。

  3.2服务器端工作流程

  服务器端通过socket()系统调用创建一个Socket数组后(即设定了接受连接客户的最大数目),与指定的本地端口绑定bind(),就可以在端口进行侦听listen()。如果有客户端连接请求,则在数组中选择一个空Socket,将客户端地址赋给这个Socket。然后登录成功的客户就可以在服务器上聊天了。

  3.3客户端工作流程

  客户端程序相对简单,只需要建立一个Socket与服务器端连接,成功后通过这个Socket来发送和接收数据就可以了。

4.核心代码分析

  限于篇幅,这里仅给出与网络编程相关的核心代码,其他的诸如聊天文字的服务器和客户端显示读者可以自行添加。

  4.1服务器端代码

  开启服务器功能:

void OnServerOpen() //开启服务器功能

 WSADATA wsaData;
 int iErrorCode;
 char chInfo[64];
 if (WSAStartup(WINSOCK_VERSION, &wsaData)) //调用Windows Sockets DLL
  { MessageBeep(MB_ICONSTOP);
   MessageBox("Winsock无法初始化!", AfxGetAppName(), MB_OK|MB_ICONSTOP);
   WSACleanup();
   return; }
 else
  WSACleanup(); 
  if (gethostname(chInfo, sizeof(chInfo)))
  { ReportWinsockErr("\n无法获取主机!\n ");
   return; }
  CString csWinsockID = "\n==>>服务器功能开启在端口:No. ";
  csWinsockID += itoa(m_pDoc->m_nServerPort, chInfo, 10);
  csWinsockID += "\n";
  PrintString(csWinsockID); //在程序视图显示提示信息的函数,读者可自行创建
  m_pDoc->m_hServerSocket=socket(PF_INET, SOCK_STREAM, DEFAULT_PROTOCOL); 
  //创建服务器端Socket,类型为SOCK_STREAM,面向连接的通信
  if (m_pDoc->m_hServerSocket == INVALID_SOCKET)
  { ReportWinsockErr("无法创建服务器socket!");
   return;}
  m_pDoc->m_sockServerAddr.sin_family = AF_INET;
  m_pDoc->m_sockServerAddr.sin_addr.s_addr = INADDR_ANY; 
  m_pDoc->m_sockServerAddr.sin_port = htons(m_pDoc->m_nServerPort);
  if (bind(m_pDoc->m_hServerSocket, (LPSOCKADDR)&m_pDoc->m_sockServerAddr,   
     sizeof(m_pDoc->m_sockServerAddr)) == SOCKET_ERROR) //与选定的端口绑定
   {ReportWinsockErr("无法绑定服务器socket!");
    return;}
   iErrorCode=WSAAsyncSelect(m_pDoc->m_hServerSocket,m_hWnd,
   WM_SERVER_ACCEPT, FD_ACCEPT);
   //设定服务器相应的网络事件为FD_ACCEPT,即连接请求,
   // 产生相应传递给窗口的消息为WM_SERVER_ACCEPT
  if (iErrorCode == SOCKET_ERROR) 
   { ReportWinsockErr("WSAAsyncSelect设定失败!");
    return;} 
  if (listen(m_pDoc->m_hServerSocket, QUEUE_SIZE) == SOCKET_ERROR) //开始监听客户连接请求
   {ReportWinsockErr("服务器socket监听失败!");
    m_pParentMenu->EnableMenuItem(ID_SERVER_OPEN, MF_ENABLED);
    return;}
  m_bServerIsOpen = TRUE; //监视服务器是否打开的变量
 return; 
}
  响应客户发送聊天文字到服务器:ON_MESSAGE(WM_CLIENT_READ, OnClientRead)

LRESULT OnClientRead(WPARAM wParam, LPARAM lParam)
{
 int iRead;
 int iBufferLength;
 int iEnd;
 int iRemainSpace;
 char chInBuffer[1024];
 int i;
 for(i=0;(i   //MAXClient是服务器可响应连接的最大数目
  {}
 if(i==MAXClient) return 0L;
  iBufferLength = iRemainSpace = sizeof(chInBuffer);
  iEnd = 0;
  iRemainSpace -= iEnd;
  iBytesRead = recv(m_aClientSocket[i], (LPSTR)(chInBuffer+iEnd), iSpaceRemaining, NO_FLAGS);   //用可控缓冲接收函数recv()来接收字符
  iEnd+=iRead;
 if (iBytesRead == SOCKET_ERROR)
  ReportWinsockErr("recv出错!");
  chInBuffer[iEnd] = '\0';
 if (lstrlen(chInBuffer) != 0)
  {PrintString(chInBuffer); //服务器端文字显示
   OnServerBroadcast(chInBuffer); //自己编写的函数,向所有连接的客户广播这个客户的聊天文字
  }
 return(0L);
}
  对于客户断开连接,会产生一个FD_CLOSE消息,只须相应地用closesocket()关闭相应的Socket即可,这个处理比较简单。

  4.2客户端代码

  连接到服务器:

void OnSocketConnect()
{ WSADATA wsaData;
 DWORD dwIPAddr;
 SOCKADDR_IN sockAddr;
 if(WSAStartup(WINSOCK_VERSION,&wsaData)) //调用Windows Sockets DLL
 {MessageBox("Winsock无法初始化!",NULL,MB_OK);
  return;
 }
 m_hSocket=socket(PF_INET,SOCK_STREAM,0); //创建面向连接的socket
 sockAddr.sin_family=AF_INET; //使用TCP/IP协议
 sockAddr.sin_port=m_iPort; //客户端指定的IP地址
 sockAddr.sin_addr.S_un.S_addr=dwIPAddr;
 int nConnect=connect(m_hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)); //请求连接
 if(nConnect)
  ReportWinsockErr("连接失败!");
 else
  MessageBox("连接成功!",NULL,MB_OK);
  int iErrorCode=WSAAsyncSelect(m_hSocket,m_hWnd,WM_SOCKET_READ,FD_READ); 
  //指定响应的事件,为服务器发送来字符
 if(iErrorCode==SOCKET_ERROR)
 MessageBox("WSAAsyncSelect设定失败!");
}
  接收服务器端发送的字符也使用可控缓冲接收函数recv(),客户端聊天的字符发送使用数据可控缓冲发送函数send(),这两个过程比较简单,在此就不加赘述了。

  5.小结

  通过聊天室程序的编写,可以基本了解Windows Sockets API编程的基本过程和精要之处。本程序在VC++6.0下编译通过,在使用windows 98/NT的局域网里运行良好。

使用TCP堆栈来Ping计算机

以下的这个类是解决一个很普通的问题的:在一台WIN95的计算机上怎么样利用MSTCP堆栈去PING另外一台计算机。当然,这个类在NT3.51和NT4上也可以用。显然,MicroSoft公司不会那么笨,在WIN系统中又另外构造这么一个单独的机制来解决这个问题,让本来就复杂的WIN系统更加复杂。那么,我们只能用ICMP DLL自己来解决这个问题了。不过,很让人失望,MicroSoft公司直到Winsock 2.0也没有解决这个问题。

  难题就是:给一个计算机的名字,或者一台计算机的IP地址,怎么样去PING它,而且得到它的响应时间。所以我们用了ICMP DLL,而对于一些SOCKET结构来说,在CSocket中已经全部都有定义了。所以,下面的类是一个CSocket的子类,它将会有更好的功能来解决更多的问题。不过你得先得到ICMPAPI.H,ICMAPI.CPP,ICMP.LIB和IPEXPORT.H,IPEXPORT.CPP这些文件,这些文件应该加在你的工程中。这些文件你可以在Microsoft Developers Network的光盘上得到,不过在微软公司的主页上也能拿到,而且更新。

  类中有4个公共函数,如下:

  unsigned long ResolveIP(CString strIP)
  unsigned long ResolveName(CString strHostName)
  CString GetIP(unsigned long ulIP)
  DWORD PingHost(unsigned long ulIP, int iPingTimeout)

  ResolveIP(CString strIP)函数使用一个IP地址的字符串来作为参数,返回值是IP地址值。

  ResolveName(CString strHostName)函数使用一计算机名的字符串来作为参数,经过DNS或者WINS的解析,返回值是被PING计算机的IP 地址,注意它使用了GetHostByName模块化。

  GetIP(unsigned long ulIP)函数是以IP地址作为参数(注意是IP地址),返回值是一个表示IP地址的字符串。

  PingHost(unsigned long ulIP, int iPingTimeout)函数,第1个参数是IP地址(注意是IP地址,而不是IP地址的字符串),第2个参数是表示时间值的,表示间隔时间的。函数功能是,去PING一台计算机,返回值是PING的响应时间。

  以下是代码:

  //*
  //-------------------------------------------------------------------------
  //-------------------------------------------------------------------------
  //icmpecho.h
  //-------------------------------------------------------------------------
  //------------------------------------------------------------------------
  //*
  class CIcmpEcho : public CSocket
  {
  // Attributes
  public:

  // Operations
  public:
  CIcmpEcho();
  virtual ~CIcmpEcho();

  unsigned long ResolveIP(CString strIP);
  unsigned long ResolveName(CString strHostName);

  DWORD PingHost(unsigned long ulIP, int iPingTimeout);

  CString GetIP(unsigned long ulIP);

  // Overrides
  public:
  // ClassWizard generated virtual function overrides
  //{{AFX_VIRTUAL(CIcmpEcho)
  //}}AFX_VIRTUAL

  // Generated message map functions
  //{{AFX_MSG(CIcmpEcho)
  // NOTE - the ClassWizard will add and remove member functions here.
  //}}AFX_MSG

  // Implementation
  protected:
  };
  /////////////////////////////////////////////////////////////////////////////
  //*
  //-------------------------------------------------------------------------------------------  -----------------------
  //icmpecho.cpp
  //-------------------------------------------------------------------------------------------  -----------------------
  //*
  // IcmpEcho.cpp : implementation file
  //

  #include "IcmpEcho.h"

  extern "C" {
  #include "ipexport.h"
  #include "icmpapi.h"
  }

  #define PING_TIMEOUT 100

  #ifdef _DEBUG
  #define new DEBUG_NEW
  #undef THIS_FILE
  static char THIS_FILE[] = __FILE__;
  #endif

  /////////////////////////////////////////////////////////////////////////////
  // CIcmpEcho

  CIcmpEcho::CIcmpEcho()
  {
  }

  CIcmpEcho::~CIcmpEcho()
  {
  }

  // Do not edit the following lines, which are needed by ClassWizard.
  #if 0
  BEGIN_MESSAGE_MAP(CIcmpEcho, CSocket)
  //{{AFX_MSG_MAP(CIcmpEcho)
  //}}AFX_MSG_MAP
  END_MESSAGE_MAP()
  #endif // 0

  /////////////////////////////////////////////////////////////////////////////
  // CIcmpEcho member functions
  unsigned long CIcmpEcho::ResolveIP(CString strIP)
  {
  //Task 1: Given IP Address i.e. "111.111.111.111",
  // Return Network byte ordered address (ulIP)

  unsigned long ulIP;

  ulIP =(IPAddr)inet_addr(strIP);

  return ulIP;
  }

  unsigned long CIcmpEcho::ResolveName(CString strHostName)
  {
  //Task 1: Resolve HostName (through DNS or WINS, whichever appropriate)
  //Task 2: Return network byte ordered address (ulIP)

  unsigned long ulIP;
  hostent* phostent;

  phostent = gethostbyname(strHostName);

  if (phostent == NULL)
  return 0;

  ulIP = *(DWORD*)(*phostent->h_addr_list);

  return ulIP;

  }

  DWORD CIcmpEcho::PingHost(unsigned long ulIP, int iPingTimeout)
  {
  //Task 1: Open ICMP Handle
  //Task 2: Create Structure to receive ping reply
  //Task 3: SendPing (SendEcho)
  //Task 4: Close ICMP Handle
  //Task 5: Return RoundTripTime

  unsigned long ip = ulIP;

  HANDLE icmphandle = IcmpCreateFile();

  char reply[sizeof(icmp_echo_reply)+8];

  icmp_echo_reply* iep=(icmp_echo_reply*)&reply;
  iep->RoundTripTime = 0xffffffff;

  IcmpSendEcho(icmphandle,ip,0,0,NULL,reply,sizeof(icmp_echo_reply)+8,iPingTimeout);

  IcmpCloseHandle(icmphandle);

  return iep->RoundTripTime;

  }

  CString CIcmpEcho::GetIP(unsigned long ulIP)
  {
  //Task 1: Given a host order ulIP Address
  // Return a IP address in format of xxx.xxx.xxx.xxx

  LPSTR szAddr;

  struct in_addr inetAddr;

  inetAddr.s_addr = (IPAddr)ulIP;

  szAddr = inet_ntoa(inetAddr);

  CString csIP = szAddr;

  return csIP;

  } 

用VC++实现上网拨号功能

现在的时代是网络的时代,网络应用程序的应用越来越广泛,在自己设计的应用程序中实现直接拨号上网,给应用程序的使用者带了很多方便,本应用程序就是在中文windows95操作系统上,用VC++5.0开发的实现拨号上网的一个小程序,希望能对大家有所帮助。下面是实现这一功能的源程序: 
---- 首先创建一个以对话框为主的VC++工程,以下是本应用程序用的对话框(图略) 

---- 然后在对话框这个类的头文件中加入几个新定义函数: public:

void SetupDialupCombo(void);
protected:
//{{AFX_MSG(CDialprocDlg)
virtual BOOL OnInitDialog();
afx_msg void OnConnectbutton();
//响应对话框中BUTTON按钮“接续”的事件
afx_msg void OnSelchangeDialupCombo();
//响应对话中COMBO控件选择改变的事件
//}}AFX_MSG



---- 以下是在对话框这个类的源文件中加入新定义函数的功能:别忘了在头文件中加入#include < ras.h >这个头文件,以及在Settings中的Link中的LibraryModules加入rasapi32.lib. void CDialprocDlg::SetupDialupCombo()

m_DialCombo.ResetContent();
//m_DialCombo是对话框中Combox控件的命名
DWORD n;
DWORD dwSize=0;
//枚举出本地电脑中的所有拨号连接。
DWORD ret=RasEnumEntries
(NULL, NULL, NULL, &dwSize,&n);
  n=dwSize/sizeof(RASENTRYNAME);
RASENTRYNAME *entry=new RASENTRYNAME[n];
if (entry) {
   entry[0].dwSize=sizeof(RASENTRYNAME);
DWORD ret=RasEnumEntries
(NULL, NULL, entry, &dwSize,&n);
for (DWORD i=0;i< n;i++) {
m_DialCombo.AddString(entry[i].szEntryName);
}
delete[] entry;
} }
void CDialprocDlg::OnConnectbutton() 
{ CWaitCursor wait;
RASDIALPARAMS params;
memset(?ms, '\0', sizeof(params));
params.dwSize=sizeof(params);
m_DialCombo.GetWindowText(params.szEntryName,
sizeof(params.szEntryName)-1);
strcpy(params.szPhoneNumber,"");
m_UserId.GetWindowText(params.szUserName, 
sizeof(params.szUserName)-1);
m_PasswordEdit.GetWindowText(params.szPassword,
sizeof(params.szPassword)-1);
//m_UserId 和m_PasswordEdit是对话框中两个Edit控件的命名
HRASCONN handle;
//指定的拨号连接。
if (RasDial(NULL, NULL, ?ms, 
NULL, NULL, &handle)==0) {
MessageBox("已联接,如果按“确定”就会切断电话");
} else {
MessageBox("正在拨打的计算机没有应答,稍后请再试");
}
wait.Restore();
RasHangUp(handle);
}
void CDialprocDlg::OnSelchangeDialupCombo() 
{ RASDIALPARAMS params;
memset(?ms, '\0',sizeof(params));
params.dwSize=sizeof(RASDIALPARAMS);
int sel=m_DialCombo.GetCurSel();
if (sel >=0) {
m_DialCombo.GetLBText(sel, params.szEntryName); TRACE("%s\n",params.szEntryName);
BOOL p;
RasGetEntryDialParams(NULL, ?ms, &p);
m_UserId.SetWindowText(params.szUserName);
m_PasswordEdit.SetWindowText(params.szPassword);
} }
BOOL CDialprocDlg::OnInitDialog()
{CDialog::OnInitDialog();
SetupDialupCombo();
return TRUE; // return TRUE
unless you set the focus to a control
}


用VC++5.0编写Ftp客户程序

随着Internet的迅猛发展,网络软件的开发与设计显得越来越重要。最初的网络软件主要是以UNIX操作系统为软件开发环境的,随着Windows个人操作系统的流行,传统的编程界面向这一新的软硬件平台转换变得极为迫切。VC++5.0版的MFC封装了的CSocket类提供了高级的SOCKET支持,为编写因特网环境下基于Windows平台的C/S程序提供了极大的方便。本文通过利用CSocket类编写一个FTP客户程序为例介绍了其使用方法,向你揭开网络编程的秘密。

  WINSOCK以动态链接库的形式向程序员提供了一个功能强大的函数集,通过对这个函数集的调用,应用程序可以完成其特定的任务。然而缺点是程序较为繁琐。为了解决这一问题,Microsoft对其推出的Visual C++系列的基本类库(MFC)做了逐步的完善。尤其是新近发行的VC++5.0版,封装了许多与网络程序设计相关的类。CSocket就是其中之一。

  CSocket类(父类为CAsyncSocket)提供了一个高级的SOCKET支持,完成对低层函数的操作,大大降低了编程难度。这里,以Windows 95为开发环境,采用Visual C++5.0编写一个Ftp客户程序,来说明如何深入有效地利用CSocket类进行网络软件的开发。考虑到C/S模式下应建立一个Ftp服务器的问题,所以选择Windows 95的4.00.950B版,因为这个版本含有个人Web服务器,提供了HTTP及FTP服务。

  首先,建立一个SDI(单文档界面)应用程序的基本框架。这一步比较简单,在VC++5.0中,MFC AppWizard通过创建一个新的项目(Project)而被激活,选择File菜单中的New选项,选取Project,输入文件名为SuperFTP,选择OK。随后的步骤为VC++自动创建过程,可以参见相关资料,不再详述。最后生成以下几个主要类:
CMainFrame,
CSuperFTPApp,
CSuperFTPDoc,
CSuperFTPView,
CAboutDlg。
  其次,建立几个新类,如下表:
  有关Ftp协议请参考相关资料,这是正确开发Ftp客户程序的重要前提。
  第三步,具体程序的编制。由于整个程序比较长,下面给出主要部分的核心代码并附注释。
1.MainFrm.cpp:
……
CMainFrame::CMainFrame()

//初始化指针
m_ctrlconn=NULL;
m_dataconn=NULL;
m_recvconn=NULL;

//选择菜单项“快速连接”
void CMainFrame::OnQuickconnect()

if(!Makeconn())
MessageBox(“FTP控制链路建立失败!”,“提示”,MB_ICONWARNING);
if(!MakeRemoteDir())
MessageBox(“FTP数据链路建立失败!”,“提示”,MB_ICONWARNING);

//建立控制链路
BOOL CMainFrame::Makeconn()

……
Quickconn dlg;
//输入服务器名,用户名,口令
if (dlg.DoModal()==IDOK)

fservername=dlg.m_servername;
fusername=dlg.m_username;
fpassword=dlg.m_password;

m_ctrlconn=new ctrlsocket();
//建立一个SOCKET
If(!m_ctrlconn->Create(0,SOCK_STREAM,NULL)

delete m_ctrlconn;
m_ctrlconn=NULL;
MessageBox(“Socket()建立失败!”,“提示”,MB_ICONWARNING);
return FALSE;

//申请网络事件通知
If(!m_ctrlconn->AsyncSelect(FD_READ|FD_WRITE|FD_ACCEPT|FD_CONNECT|FD_CLOSE))

MessageBox(“AsyncSelect()错误!”,“提示”,MB_ICNWARNING);
return FALSE;

BeginWaitCursor();
//向由fservername指定的主机发出连接请求
if(!m_ctrlconn->Connect(fservername,IPPORT_FTP))

delete m_ctrlconn;
m_ctrlconn=NULL;
MessageBox(远端服务器连接失败!”,“提示”,MB_ICONWARNING);
return FALSE;

EndWaitCursor();
……
return TRUE;

  如果选择了“匿名”登录,应用程序则自动将用户姓名和口令字填充为:anonymous,guest@unknown
BOOL CMainFrame::MakeRemoteDir()

if(m_ctrlconn==NULL)

(“请连接到服务器!”,“提示”,MB_iconwarwing);
retrurn FALSE;

if(!MakeDataListen())

MessageBox(“数据链路建立失败!”,“提示”,MB_ICONWARNING);
return FALSE;

……
return TRUE;

//处理来自远端服务器的响应
BOOL CMainFrame::Processftp()

………

//建立数据连接“监听”函数
BOOL CMainFrame::MakeDataListen()

……
m_dataconn=new Listensocket();
/* 建立一个SOCKET,并与本地主机地址捆绑。作为一个例子,直接插入了本机的IP地址“90.0.0.8”,而在实际应用中应首先通过相应的函数调用取得本地主机地址。

  注意:如果不采用VC++的CSocket类而用其它的方法,需在建立SOCKET之后,调用bind()函数来进行与本地主机地址的捆绑。*/
if(!m_dataconn->Create(0,SOCK_STREAM,“90.0.0.8”))

delete m_dataconn;
m_dataconn=NULL;MessageBox(“Socket()建立失败!”,“提示”,MB_ICONWARNING);
return FALSE;

//申请网络事件通知
if(!m_dataconn->AsyncSelect(FD_ACCEPT))

delete m_dataconn;
m_dataconn=NULL;
MessageBox(“AsyncSelect()错误!”,“提示”,MB_ICONWARNING);
return FALSE;

……
if(!m_dataconn->Listen(5))

MessageBox(“listen()错误!”,“提示”,MB_ICONWARNING);
return FALSE;

……

//接受数据连接请求的函数
BOOL CMainFrame::AcceptDataConn()

int num,nRet;
SOCKADDR_IN RemoteDataAddr;
num=sizeof(SOCKADDR_IN);
if(m_recvconn==NULL)

m_recvconn=new Datasocket();

if(!m_dataconn->Accept(*m_recvconn,(LPSOCKADDR)&RemoteDataAddr,(int FAR*)&num))

MessageBox(“accept()错误!”,“提示”,MB_ICONWARWING);
return FALSE;

……
m_dataconn->Close();
//申请网络事件通知
if(!m_recvconn->AsyncSelect(FD_READ|FD_WRITE|FD_CLOSE)

MessageBox(“Asyncselect()错误!”,“提示”,MB_ICONWARNING);
return FALSE;

return TRUE;

//数据接受函数,接受来自远端服务器的数据
int CMainFrame::RecvData()

……
int nRet=m_recvconn->Receive(…);
if (nRet>0)



else



……

2.ctrlsocket.cpp
……
void ctrlsocket::OnReceive(int nErrorCode)

//处理网络事件FD_READ
CSocket::OnReceive(nErrorCode);
((CmainFrame *)(AfxGetApp()->m_pMainWnd))->Processftp();

3.Listensocket.cpp
……
void Listensocket::OnAccept(int nErrorCode)

//处理网络事件FD_ACCEPT
CSocket::OnAccept(nErrorCode);
((CmainFrame*)AfxGetApp()->m_pMainWnd))->AcceptDataConn();

4.Datasocket.cpp
……
Datasocket::OnReceive(int nErrorCode)

//处理网络事件FD_READ
CAsyncSocket::OnReceive(nErrorCode);
((CMainFrame*)(AfxGetApp()->m_pMainWnd))->RecvData();

void Datasocket::OnSend(int nErrorCode)

//处理网络事件FD_WRITE
CAsyncSocket::OnSend(nErrorCode);
((CMainFrame*)(AfxGetApp()->m_pMainWnd))->SendData();



  程序执行时左下窗口为本地系统,右下窗口为远端系统。通过激活相关的菜单项,就可以对远端主机的文件系统进行下载操作。当然,如果Ftp服务器允许上载,你也可以将自己的文件传送到远端主机。

用VC++制作一个简单的局域网消息发送工程

本工程类似于oicq的消息发送机制,不过他只能够发送简单的字符串。虽然简单,但他也是一个很好的VC网络学习例子。

  本例通过VC带的SOCKET类,重载了他的一个接受类mysock类,此类可以吧接收到的信息显示在客户区理。以下是实现过程:

  建立一个MFC 单文档工程,工程名为oicq,在第四步选取WINDOWS SOCKetS支持,其它取默认设置即可。为了简单,这里直接把about对话框作些改变,作为发送信息界面。 

  这里通过失去对话框来得到发送的字符串、获得焦点时把字符串发送出去。创建oicq类的窗口,获得VIEW类指针,进而可以把接收到的信息显示出来。

extern CString bb;
void CAboutDlg::OnKillFocus(CWnd* pNewWnd) 
{
 // TODO: Add your message handler code here
 CDialog::OnKillFocus(pNewWnd);
 bb=m_edit; 
}
对于OICQVIEW类
char aa[100];
CString mm;
CDC* pdc;
class mysock:public CSocket //派生mysock类,此类既有接受功能
{public:void OnReceive(int nErrorCode) //可以随时接收信息
 {
  CSocket::Receive((void*)aa,100,0);
  mm=aa;
  CString ll=" ";//在显示消息之前,消除前面发送的消息
  pdc->TextOut(50,50,ll);
  pdc->TextOut(50,50,mm);
 }
};

mysock sock1;
CString bb;
BOOL COicqView::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{
 CView::OnSetFocus(pOldWnd);

 // TODO: Add your message handler code here and/or call default
 bb="besting:"+bb; //确定发送者身份为besting
 sock1.SendTo(bb,100,1060,"192.168.0.255",0); //获得焦点以广播形式发送信息,端口号为1060

 return CView::OnSetCursor(pWnd, nHitTest, message);
}

int COicqView::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
 if (CView::OnCreate(lpCreateStruct) == -1)
  return -1;
  sock1.Create(1060,SOCK_DGRAM,NULL);//以数据报形式发送消息

  static CClientDC wdc(this); //获得当前视类的指针
  pdc=&wdc; 
  // TODO: Add your specialized creation code here

  return 0;
}
运行一下,打开ABOUT对话框,输入发送信息,enter键就可以发送信息了,是不是有点像qq啊?

设计网络服务器ISAPI

阅读本文需要具备基本的Web/CGI 、MFC 以及Visual C++ v4.1或更新版本的知识。 

  ISAPI 与CGI

  很长时期以来,CGI都作为交互web开发的一个标准工具。CGI脚本允许使用者用各种语言来编写简单的应用程序,它在网络服务器上运行并直接输出到用户的网络浏览器上。用户的数据通过环境变量或者标准输入设备输入,程序则会通过标准输出返回HTML文本。这么一个简单的设计,就结合了Perl及TCL语言,也使得CGIs非常简单易用。

  但CGIs也有一个非常大的缺点:性能问题。虽然有很多方法让CGI运行得更快(例如:用户可以把CGI写成可执行的可编译的语句,而不是PERL脚本),但速度仍是问题所在。每次进入CGI都得通过网络,可执行CGI仍必须为每一次进入请求创建新的程序。对于一个访问量大的站点来说,上述方法对于服务器无疑是个巨大的负担。

  当微软开始研究他们的网络服务器(MS IIS 或者是 IIS)时意识到CGIS对于大型的网络服务器来说是个主要的问题所在。 

  一、进入ISAPI

  事实上,ISAPI使用DLL。所用的DLL则会被加载进服务器中。将代码缓存进内存的作法替代了每请求一次重新加载的做法,此种技术的应用正呈上升趋势。

  ISAPI 的优点

  速度

  在功能上此优点得到最大体现。

  特征

  ISAPI可以创建服务器过滤器。完全由MFC集成。

  ISAPI的不足

  标准性

  目前只有少数几种服务器支持ISAPI

  开发的简便性

  文档非常少,并且调试程序的过程比较枯燥。
二、ISAPI的基本知识

  ISA是基本于MFC ChttpServer类别的,CHttpServer 基本上控制了所有的服务器内部交互行为,并包含了用户要求的所有功能。事实上ISA能够处理大量的相类似的请求。因此,CHttpServer为每个请求都创建了ChttpServerContext。ChttpServerContext包括了所有的专业化数据以及所有的HTML。

  ISAPI DLLs是由用户的需要而开发的,用法与CGI类似。如下例"

  http://www.mysite.com/myisa.dll?name=bob&id=15248

  "名称"和"ID"域以及相关数据都进入了ISA,并且数据必须在使用以前被放置进数据库存中。为加快ISAPI则需要用到 "mapping"系统。

  "mapping"系统同样具有其它功能:ISAPI能引导 请求 到ISA内的专用功能区。"请求"串里包含了可以令"mapping"系统用于引导"请求"至适当功能区的命令。

  因为ISAPI使用了处理"请求"的指令,因此ISA开发系统就给人感觉有点迟缓。但是只要一旦掌握,确是一个处理"请求"的强有力的工具。

  设置项目

  开发ISA的第一步是建立一个项目工作区。正如其它由Visual C++ (VC++)创立的工作区一样,有工作向导引导用户完成最初步骤。选择New-Project Workspace,选择"ISAPI Extension Wizard"作为项目种类,命名为"Hello Web",然后点击创建。

  完成上述步骤后,会跳出对话框询问您愿意创建哪种类型的ISA。缺省设置已为ISA配置好,MFC会动态的通过缺省连接。如果您的服务器已安装MFCDLLS,则上述步骤适用,如果没有则ISA不会运行。如果项目需要静态连接。完成上述步骤后,点击"完成"。 Visual C++ 会提醒您文件正创建中,并会生成ChelloWebExtension。本文里的所有工作都将在CHelloWebExtension"里完成。

  现在你已建立好一个项目,是时候完成一些ISAPI开发工作了。正如早先提到的,ISA在运行时则会成为IIS的一部份。IIS依次序的运行就象NT服务器一样。这样则会使得调试过程变得复杂化。因为VC++的调试系统不能够控制ISA,当服务器作为一个服务系统时。为了解决这个问题,微软将IIS分成两个部份,一个是service,,另一个是可执行的。通过可执行的部份,利用指令行可调节服务器。虽然问题得到了解决并使得开发更为简便,但是设置以上步骤的过程却有点枯燥。

  当你进入调试部份,在用户的允许状态下,VC++ (然后是 IIS)会在用户的帐户下运行。然而有些是用户的指令得不到进入但IIS得执行的部份,因此用户得做以下步骤:

  打开User Manager域工具组(在Administrative Tools program 组)

  在Policies菜单中选择User Rights

  打开Show Advanced User Rights栏

  在右边列表中选择Act as part of the operating system

  点击Add按钮以弹出Add Users and Groups对话框。点击Show Users按钮,选择您需要的帐户。然后点击Add.

  重复相同步骤以生成Generate security audits rights。

  以上步骤完成后请退出然再重新登录,以使程序生效。

  IIS包含了FTP Publishing Service, Gopher Publishing Service, World Wide Web Publishing Service等三个Service。一旦调试程序从指令行里运行IIS,则三个services则会停止运行。

  如果用户想让程序调试得到合理分配,最好关掉IIS service,转而使用Services Control Applet,并且禁止自动重启功能。

  一旦当Service关闭,则项目工作区则需按以下步骤来配置

  从Build菜单中选择Settings

  点击Debug标签并选择"General Category". 

  在"Executable for debug session"域中键入可执行IIS的位置

  在"Program arguments" field中键入"-e w3svc"

  点击Link 标签。

  在"Output filename" field中键入路径和文件名。路径会在站点的目录树里显示,因此可以通过URL进入。例如:您的站点根目录是c:\www\,并且您将"helloweb.dll"放在根目录里,因此URL则会是:

  http://www.mysite.com/helloweb.dll

请在更改设置后退出登录并重新登录。

  由ISAPI Extension 向导生成的默认设置包括了编译ISA的所有细节。现在您已完成配置调试程序的环境,现在可以创建并运行项目。

  按F5以打开ISA,当系统询问是否建立项目时,按YES。

  在程序调试创建的几秒后,IIS会在后台运行。

  后将DLL的URL输入你喜爱的网络浏览器,记住在尾部加入一个问号。

  则URL会显示如下:

   http://www.mysite.com/helloweb.dll?

  第一次连接到ISA会耗费上几秒。但是DLLS会在执行后进行缓存,因此速度会变得稳定。

  在DLL登录后,将会显示以下信息:

  This default message was produced by the Internet Server DLL Wizard.

  Edit your CHelloWebExtension::Default() implementation to change it.

  现在你已得到一个工作的ISA

  Walking through the base code

当EXTENSION_CONTROL_BLOCK提出请求时,则会传送到Command Parse Map.。 Parse Map是由一系列的宏所定义的,正如下面所举的代码,是从Hello Web项目中复制过来的:

  ISA有两个主要元素:Parse Map 以及 Command Handler 功能。

  BEGIN_PARSE_MAP(CHelloWebExtension, CHttpServer)

  // TODO: insert your ON_PARSE_COMMAND() and 

  // ON_PARSE_COMMAND_PARAMS() here to hook up your commands.

  // For example:

  ON_PARSE_COMMAND(Default, CHelloWebExtension, ITS_EMPTY)

  DEFAULT_PARSE_COMMAND(Default, CHelloWebExtension)

  END_PARSE_MAP(CHelloWebExtension)

  BEGIN_PARSE_MAP标注了parse map的起始处,ISA's ChttpServer 和基类CHttpServer作为参数。

  ON_PARSE_COMMAND_PARAMS()表明对指令处理器是一种特殊请求格式或命令,它的参数就是指向的函数的类名和请求的格式。DEFAULT_PARSE_COMMAND说明那个函数被调用,参数是被调用的类名。

  Command Handler functions是主函数ChttpServer类的成员函数,parse map通过get方法调用CHttpServer。下面则是Hello Web 的"缺省"指令处理程序:


void CHelloWebExtension::Default(CHttpServerContext* pCtxt)
{
StartContent(pCtxt);
WriteTitle(pCtxt);
*pCtxt << _T("This default message was produced by the Internet");
*pCtxt << _T("Server DLL Wizard. Edit your CHelloWebExtension::Default()");
*pCtxt << _T("implementation to change it.\r\n");
EndContent(pCtxt);
}

  当请求为空或包含"Default"这个函数被调用,请求通过ChttpServerContext进入程序体,第一个参数必须是一个ChttpServerContext对象。StartContent()方法在pCtxt中放置<HTML><BODY>,WriteTitle()则是放置 <title>标记。下面 三行语句则写入缺省消息,并使pCtxt指向ChtmlStream,在ISA处理完成后发送到客户端。

   Hello Web 

  第一个程序会以"HELLO WEB"替换缺省信息串。

  找到CHelloWebExtension class 中的Default() 成员函数,按照下面的方式更改。

void CHelloWebExtension::Default(CHttpServerContext* pCtxt)
{

StartContent(pCtxt);
WriteTitle(pCtxt);
*pCtxt << _T("Hello Web!");
EndContent(pCtxt); 
}

  创建,运行DLL并从网络浏览器重新载入并替换DLL

  缺省信息为:

  produced by the InternetServer DLL Wizard. Edit

  your CHelloWebExtension::Default() implementation to change it.

  将会显示:

  Hello Web!

  如果出现"Server Error 500: Specified module not found."提示,您所创建的项目是动态连接,而且缺少必要的DLLs。纠正这个错误,就必须使用MFC重新静态的连接项目。

通讯篇

RS-232-C端口实时监控软件的设计实现

摘要:

  本文介绍了在Microsoft Visual C++ 6.0环境下对RS-232-C串行端口进行编程,以及对后台监控程序所普遍涉及到的无阻塞后台运行、数据的实时接收和处理等问题的解决方法。

  一、 引言

  在实验室和工业应用中,受信道成本限制,串口常常作为计算机与外部串行设备之间的首选数据传输通道,而且由于串行通信方便易行,许多设备和计算机都可以通过串口对外设进行控制、检测,串口通讯日益成为计算机和外设进行通讯、获取由外设采集到的监测数据的一个非常重要的手段。本文所描述的程序实例运行于Windows 9x操作系统下,可后台运行、实时接收、处理从端口传来的数据,并能通过向串口发送命令来控制外设的动作。为了避免在实时监控数据时引发程序阻塞,在本程序中引入了线程和端口中断响应等技术。

  二、 程序设计思路

  由于本程序要对串行端口进行实时监控,这就要求它是一个后台程序,在监控的同时可以在前台进行其他一些于之无关的操作。因而在实现时即要避免无时无刻都在反复读端口的效率低下的轮询方式,又不能因为来不及处理而将突然到达的监测数据丢失。只有采取端口中断的异步方式才能实现高效、安全的监控过程,只要一有数据到达端口,马上抛出中断请求,中断处理函数便会及时启动以处理到来的数据,从而避免了轮询间隙丢时数据的可能。而在大部分无数据到达的时间内不会有中断抛出,中断处理函数也不会执行,即仅仅在有数据到达的一瞬间进行工作,此效率不可谓不高。

  综上所述,要实现上述要求,就要用到下列技术来解决所遇到的关键性问题:一是采用多线程来避免在进行文件操作等耗时操作时会引发阻塞现象的发生。同时为了防止多个线程同时对同一个变量进行操作引起时序上的差错,为了保持线程的同步,还采取了临界区加解锁的技术;二是对端口的数据读取方式采取中断响应模式,具体原因前面以讲的很清楚,在此不再赘述;三是使用了定时器,以满足实时监控类程序的实时显示功能,以便及时的将所接收到的动态数据及时的反映到屏
幕上。

三、 RS-232-C串行端口监控软件的程序实现

  (一) 界面风格

  由于是实时监控软件,那就既要监测从外设传来的实时数据,又要通过串口向外设发送一些具体的指令以控制外设完成预先设定的动作。为了方便向串口发送命令可以在工具条上再加一个类似于"Internet Explorer 浏览器"风格的对话条,可以在初建工程时指定"Internet Explorer ReBars"风格,也可以通过添加Microsoft Visual C++ 6.0自带的"Dialog Bar"组件来实现。而要及时将从外部读取的数据显示给管理人员,并且留有相当记录以备查阅,可以选择列表视图来实现。

  (二) 串口的参数设置及打开 

  对RS-232-C串行端口进行参数配置是使用串口进行通讯的必要条件。而且由于场合不同、用途、功能的不同对串口也采取不同的配置方式,为了使本程序更灵活,适应面更广,采取将所有的可能参数都预先设置在几个组合框中,可以在程序运行后随时更改设置。自定义一个设置串口参数的数据结构:

typedef struct tagCOM_CONFIG
{
int nPort; file://端口号,从COM1到COM4
int nBaud; file://波特率,从1200bps到57600bps(对应的宏为CBR_1200到CBR_57600)
int nData; file://数据位个数,7位或是8位
int nStop; file://停止位个数,可以是1位、1.5位、2位。
int nParity;//采取的校验方式,有无校验(NOPARITY)、
file://奇校验(ODDPARITY)和偶校验(EVENPARITY)等。
}COM_CONFIG;
  当选择好适当的参数后就可以根据设置好的端口配置情况打开通讯端口了。与以往DOS下串行通信程序不同的是,Windows操作平台下不提倡应用程序直接控制硬件(包括端口),也不让使用中断(除非打入到Ring0系统级),而是通过Windows操作系统提供的设备驱动程序来进行数据传递。在Windows操作系统下串行口和其他通讯端口一样是作为文件来进行处理的,而不是直接对端口进行操作,对于串行通信,Win 32 提供了相应的文件I/O函数与通信函数,通过了解这些函数的使用,可以编制出符合不同需要的通信程序。与通信设备相关的结构有COMMCONFIG ,COMMPROP,COMMTIMEOUTS,COMSTAT,DCB,MODEMDEVCAPS,MODEMSETTINGS共7个,与通信有关的Windows API函数共有26个,具体说明可参考MSDN帮助文件。下面是打开串口的部分关键代码:

//以创建文件的形式打开文件,并将返回的端口句柄保存于句柄idComDev之中。
idComDev =CreateFile( g_szCom_Port[g_com_config.nPort], 
GENERIC_READ | GENERIC_WRITE,
0, 
NULL, 
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); 
…… 
file://cfg为COMMCONFIG结构的实例对象,获取当前通讯口的状态。
cfg.dcb.DCBlength = sizeof( DCB ) ;
GetCommState( idComDev, &(cfg.dcb) ) ;
file://设置发送、接收缓存大小
SetupComm( idComDev, 4096, 4096 ) ;
// PurgeComm()是一个清除函数,它可以用来中止任何未决的后台读或写,并且可以冲掉I/O
file://缓冲区.其中:PURGE_TXABORT 用于中止后台写操作;PRUGE_RXABORT用于中止后台
file://读操作 ;PRUGE_TXCLEAR用于清除发送缓冲区;PRUGE_RXCLEAR用于清除接收缓冲区
PurgeComm(idComDev,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
file://根据设置的参数填充DCB结构对象dcb的各个数据成员变量
dcb.DCBlength = sizeof( DCB ) ;
GetCommState( idComDev, &dcb ) ;
file://设置端口通讯参数
dcb.BaudRate =g_Com_Baud[g_com_config.nBaud];
dcb.ByteSize =g_Com_ByteSize[g_com_config.nData];
dcb.Parity =g_Com_Parity[g_com_config.nParity] ;
dcb.StopBits =g_Com_StopBits[g_com_config.nStop];
file://硬件流控制
dcb.fDtrControl = DTR_CONTROL_DISABLE ;
dcb.fOutxCtsFlow = FALSE ;
dcb.fRtsControl = RTS_CONTROL_DISABLE ;
file://软件流控制
dcb.fInX = dcb.fOutX = FALSE ;
dcb.XonChar = (char)0xFF ;
dcb.XoffChar = (char)0XFF ;
dcb.XonLim = 100 ;
dcb.XoffLim = 100 ;
dcb.EvtChar=0x0d;
dcb.fBinary = TRUE ;
dcb.fParity = TRUE ;

file://超时控制的设置。超时有两种:区间超时:(仅对从端口中读取数据有用)它指定在读取两个字符之间要经历的时间;总超时: 当读或写特定的字节数需要的总时间超过某一阈值时,超时触发。计算超时可以根据公式:
file://ReadTotalTimeout = (ReadTotalTimeoutMultiplier * bytes_to_read)+ 
// ReadToTaltimeoutConstant
file://WriteTotalTimeout = (WriteTotalTimeoutMuliplier * bytes_to_write)+ 
// WritetoTotalTimeoutConstant
file://如果在设置超时时参数为0则为无限等待,即无超时。
CommTimeOuts.ReadIntervalTimeout =MAXDWORD;
CommTimeOuts.ReadTotalTimeoutMultiplier =0;
CommTimeOuts.ReadTotalTimeoutConstant = 0 ;
CommTimeOuts.WriteTotalTimeoutMultiplier =2*9600/dcb.BaudRate ;
CommTimeOuts.WriteTotalTimeoutConstant = 25 ;
SetCommTimeouts(idComDev , &CommTimeOuts ) ;
file://根据设置好的dcb结构设置好通讯口的状态,并开启用于侦听端口,监视从外设传来的数
file://据的线程COMReadThreadProc。
if (SetCommState( idComDev, &dcb ))
{
m_bComPortOpen=TRUE;
g_hCom=idComDev;
AfxBeginThread(COMReadThreadProc,NULL,THREAD_PRIORITY_NORMAL);
return;
}
(三) 侦听监视线程

  当成功的打开端口之后通过执行线程开启函数AfxBeginThread(COMReadThreadProc,NULL,THREAD_PRIORITY_NORMAL);开启了一个用于侦听端口的工作线程COMReadThreadProc。其具体处理过程
如下:

UINT COMReadThreadProc(LPVOID pParam)
{
……
file://设置读端口线程执行标志的标识
g_comthread.SetReadThreadKillFlag(FALSE);
while(1)
{
file://读取端口开启状态的标识
if(TRUE==g_comthread.GetCloseCOMFlag())
{
g_comthread.SetReadThreadKillFlag(TRUE);
return 0;//正常关闭
}
file://读端口操作
dwNeedRead=500;
file://从端口读取数据到缓存中
if(!ReadFile(g_hCom,buf,dwNeedRead,&dwActRead,NULL))
{
ClearCommError(g_hCom,&dwErrorMask,&comstat);
PurgeComm(g_hCom,PURGE_RXCLEAR);
continue;
}
file://读字符加入到全局缓冲
g_comreadbuf.Add(buf,dwActRead);
Sleep(1);
}
……
return 0;
}
  其中用到的g_comthread和g_comreadbuf分别是线程类CCOMThread和读端口类COMReadBuf的实例对象。这两个类里都用类CCriticalSection m_Lock;实现了临界区技术,用以保持线程间的同步。CCOMReadBuf类的两个函数GetOneByte(……)、Add(……)分别用于从端口读取一个字符和向缓冲区添加读取的字符。其主要实现代码如下:

BOOL CCOMReadBuf::GetOneByte(BYTE *cb)
{
m_Lock.Lock();
if(m_nHead==m_nTail)
{
m_Lock.Unlock();
return FALSE;//空
}
*cb=m_readbuf[m_nTail];
if(m_nTail < m_nBufSize-1)
m_nTail++;
else
m_nTail=0;
m_Lock.Unlock();
return TRUE;//空


void CCOMReadBuf::Add(BYTE buf[],int nBytes)
{
int nt,i;
m_Lock.Lock();
for(i=0;iBR> {
nt=(m_nHead-m_nTail);
if(nt<0)
nt+=m_nBufSize;
if(nt+1==m_nBufSize)
break;//缓冲区满
m_readbuf[m_nHead]=buf[i];
if(m_nHead < m_nBufSize-1)
m_nHead++;
else
m_nHead=0;
}
m_Lock.Unlock();
}
(四) 控制命令的发送

  控制命令可以从对话条上的编辑框获取,然后就可以通过写文件形式从端口发送出去,这部分实现起来较简单,也牵扯不到线程等技术。主要的代码主要有:

……
file://从对话条获取命令行
nRead=m_wndDlgBar.GetDlgItemText(IDC_EDIT_SEND,buf,500);
file://向端口发送命令
if(nRead>0)
{
buf[nRead]=0x0d;
buf[nRead+1]=0x00;
::WriteFile(g_hCom,buf,nRead+1,&dwActWrite,NULL);
}
……
  (五) 监测信息的显示

  本程序选择了列表视图作为数据的显示途径。为了能及时的将接收到的数据反馈给监控者,在视类中通过定时器完成定时刷新的功能,可以在视类的OnCreate() 函数里用SetTimer(……)函数在程序开始执行时打开定时器,在OnDestroy()里用KillTimer(……)函数在程序退出前先关闭定时器。在定时器消息 WM_TIMER的响应函数里完成向列表控件添加最新接收到的信息。主要语句有:

……
file://获取列表视相关的列表空间的句柄
CListCtrl &ListCtrl=GetListCtrl();
file://列表有两列:收到字符的时间和对应的信息
CTime t = CTime::GetCurrentTime();
CString szTemp;
szTemp.Format("%02d:%02d:%02d",t.GetHour(),t.GetMinute(),t.GetSecond());
file://向列表添加信息
int nIndex=ListCtrl.InsertItem(0, szTemp);
if(-1!=nIndex)

m_Buf[m_nCurPoint]=0;
ListCtrl.SetItemText(nIndex,1,LPTSTR(m_Buf));
}
……
  四、 调试与检测

  现在程序已经写完,可以编译运行。我们最好先检验一下机器串口是否能正常工作,可用DOS下的Comdebug程序检查。在确认串口工作正常后,如果条件允许最好同另一台计算机或外设相连,进行检测,如笔者用的是一台高频段数传电台。如果只有一台计算机也可以进行简单的测试:将计算机串口的第2脚和第3脚短接,即自己发送、接收数据。如果接有外设,当有采集到的数据送到端口时就会在列表中将时间和信息内容记录下来,也可以在对话条中输入命令来控制外设的工作状态,完全具备实时监控软件所需的功能。

  小结:

  串行通讯在通讯领域被广泛应用,标准的RS-232-C接口已成为计算机、外设、交换机和许多通讯设备的标准接口。计算机与计算机、计算机与外设等都可以通过RS-232-C接口进行方便的连接,以实现监视、控制外设和传输数据等目的。对于其他类型的串口通讯程序本文所介绍的方法也是值得借鉴的。本程序由
Microsoft Visual C++ 6.0编译、在Windows 98下运行通过。

多线程技术在VC++串口通信程序中的应用研究

1 概述

  在现代的各种实时监控系统和通信系统中,在Windows 9X/NT下利用VC++对RS-232串口编程是常用的手段。Windows 9X/NT是抢先式的多任务操作系统,程序对CPU的占用时间由系统决定。多任务指的是系统可以同时运行多个进程,每个进程又可以同时执行多个线程。进程是应用程序的运行实例,拥有自己的地址空间。每个进程拥有一个主线程, 同时还可以建立其他的线程。线程是操作系统分配CPU时间的基本实体,每个线程占用的CPU时间由系统分配,系统不停的在线程之间切换。进程中的线程共享进程的虚拟地址空间,可以访问进程的资源,处于并行执行状态,这就是多线程的基本概念。 

  2 VC++对多线程的支持

  使用MFC开发是较普遍的VC++编程方法。在VC++6.0下,MFC应用程序的线程由CWinThread对象表示。VC++把线程分为两种:用户界面线程和工作者线程。用户界面线程能够提供界面和用户交互,通常用于处理用户输入并相应各种事件和消息;而工作者线程主要用来处理程序的后台任务。

  程序一般不需要直接创建CWinThread对象,通过调用AfxBeginThread()函数就会自动创建一个CWinThread对象,从而开始一个进程。创建上述的两种线程都利用这个函数。

  线程的终止取决于下列事件之一:线程函数返回;线程调用ExitThread()退出;异常情况下用线程的句柄调用TerminateThread()退出;线程所属的进程被终止。
3 多线程在串口通信中的应用

  3.1 串口通信对线程同步的要求

  因为同一进程的所有线程共享进程的虚拟地址空间,而在Windows 9X/NT系统下线程是汇编级中断,所以有可能多个线程同时访问同一个对象。这些对象可能是全局变量,MFC的对象,MFC的API等。串口通信的几个特点决定了必须采用措施来同步线程的执行。

  串口通信中,对于每个串口对象,只有一个缓冲区,发送和接收都要用到,必须建立起同步机制,使得在一个时候只能进行一种操作,否则通信就会出错。

  进行串口通信处理的不同线程之间需要协调运行。如果一个线程必须等待另一个线程结束才能运行,则应该挂起该线程以减少对CPU资源的占用,通过另一进程完成后发出的信号(线程间通信)来激活。

  VC++提供了同步对象来协调多线程的并行,常用的有以下几种:

   CSemaphore:信号灯对象,允许一定数目的线程访问某个共享资源,常用来控制访问共享资源的线程数量。

   Cmutex:互斥量对象,一个时刻至多只允许一个线程访问某资源,未被占用时处于有信号状态,可以实现对共享资源的互斥访问。

   CEvent:事件对象,用于使一个线程通知其他线程某一事件的发生,所以也可以用来封锁对某一资源的访问,直到线程释放资源使其成为有信号状态。适用于某一线程等待某事件发生才能执行的场合。

   CCriticalSection:临界区对象,将一段代码置入临界区,只允许最多一个线程进入执行这段代码。一个临界区仅在创建它的进程中有效。

  3.2 等待函数

  Win32 API提供了能使线程阻塞其自身执行的等待函数,等待其监视的对象产生一定的信号才停止阻塞,继续线程的执行。其意义是通过暂时挂起线程减少对CPU资源的占用。在某些大型监控系统中,串口通信只是其中事务处理的一部分,所以必须考虑程序执行效率问题,当串口初始化完毕后,就使其处于等待通信事件的状态,减少消耗的CPU时间,提高程序运行效率。

  常用的等待函数是WaitForSingleObject()和WaitForMultipleObjects(),前者可监测单个同步对象,后者可同时监测多个同步对象。

  3.3 串口通信的重叠I/O方式

  MFC对于串口作为文件设备处理,用CreateFile()打开串口,获得一个串口句柄。打开后SetCommState()进行端口配置,包括缓冲区设置,超时设置和数据格式等。成功后就可以调用函数ReadFile()和WriteFile()进行数据的读写,用WaitCommEvent()监视通信事件。CloseHandle()用于关闭串口。

  在ReadFile()和WriteFile()读写串口时,可以采取同步执行方式,也可以采取重叠I/O方式。同步执行时,函数直到执行完毕才返回,因而同步执行的其他线程会被阻塞,效率下降;而在重叠方式下,调用的读写函数会立即返回,I/O操作在后台进行,这样线程就可以处理其他事务。这样,线程可以在同一串口句柄上实现读写操作,实现"重叠"。

  使用重叠I/O方式时,线程要创建OVERLAPPED结构供读写函数使用,该结构最重要的成员是hEvent事件句柄。它将作为线程的同步对象使用,读写函数完成时hEvent处于有信号状态,表示可进行读写操作;读写函数未完成时,hEvent被置为无信号。

4 程序关键代码的实现

  程序专门建立了一个串口通信类,下面给出关键成员函数的核心代码。

BOOL InitComm file://串口初始化,这里只给出关键步骤的代码,下同
{
 HANDLE m_hComm;
 COMMTIMEOUTS m_CommTimeouts;
 m_hComm = CreateFile("COM1", file://在这里只使用串口1
  GENERIC_READ | GENERIC_WRITE, file://打开类型为可读写
  0, file://以独占模式打开串口
  NULL, file://不设置安全属性
  OPEN_EXISTING,
  FILE_FLAG_OVERLAPPED, file://重叠I/O方式
  0);
 if (m_hComm == INVALID_HANDLE_VALUE) file://打开不成功
  {return FALSE;}
 m_CommTimeouts.ReadIntervalTimeout = 1000; 
 file://进行超时设置,读者应根据自己的实际需要设置
 m_CommTimeouts.ReadTotalTimeoutMultiplier = 500;
 m_CommTimeouts.ReadTotalTimeoutConstant = 5000;
 m_CommTimeouts.WriteTotalTimeoutMultiplier = 500;
 m_CommTimeouts.WriteTotalTimeoutConstant = 5000;
 if (!SetCommTimeouts(m_hComm, &m_CommTimeouts))
  {CloseHandle(m_hComm);
   return FALSE;}
 PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT | PURGE_TXABORT); file://清缓冲
 return TRUE;
}
以上是专门针对COM1的初始化,如果要利用同一函数对不同串口初始化,则要在初始化前先进入代码临界区,以保证在某一时刻只进行一个串口的初始化。

  在串口初始化成功后,就可以建立监控线程处理串口通信事件。下面是该线程的关键代码。

UINT CommThread(LPVOID pParam) file://用于监控串口的工作者线程
{
 BOOL bResult = FALSE;
 if (m_hComm) file://查看端口是否打开,这里m_hComm同上,作者在这里做了简化
  PurgeComm(m_hComm, PURGE_RXCLEAR | PURGE_TXCLEAR | PURGE_RXABORT |    PURGE_TXABORT);
  for (;;) file://只要线程运行,就处于监视端口行为的无限循环
  {
   bResult = WaitCommEvent(m_hComm, &Event, &m_ov); 
   file://m_ov是OVERLAPPED类型的成员变量
   if (!bResult)
    { file://进行出错处理}
   else
   {
    Event = WaitForMultipleObjects(4, m_hEvent, FALSE, INFINITE);
    file://无限等待设定的事件发生,数组m_hEvent根据需要定义了须响应的接收,发送,关闭端口事件和OVERLAPPED类型的hEvent事件
    switch (Event)
    { file://读写事件的响应处理过程,在此略}
    }
    return 0;
 }
这样监控主程序就可以使用AfxBeginThread()函数来产生CommThread串口监控线程。如果要实现对所有端口的同时监控,可以分别对端口建立监控线程。

  5 小结

  作为一个机房监控系统的组成部分,本串口通信程序在VC++6.0下编译通过,在使用windows 98/NT的局域网里运行良好。

VC++ 的串口通讯

在VC++中有两种方法可以进行串口通讯。一种是利用Microsoft公司提供的ActiveX控件 Microsoft Communications Control。另一种是直接用VC++访问串口。下面将简述这两种方法。

  一、Microsoft Communications Control

  Microsoft公司在WINDOWS中提供了一个串口通讯控件,用它,我们可以很简单的利用串口进行通讯。在使用它之前,应将控件加在应用程序的对话框上。然后再用ClassWizard 生成相应的对象。现在我们可以使用它了。

  该控件有很多自己的属性,你可以通过它的属性窗口来设置,也可以用程序设置。我推荐用程序设置,这样更灵活。

   SetCommPort:指定使用的串口。

   GetCommPort:得到当前使用的串口。

   SetSettings:指定串口的参数。一般设为默认参数"9600,N,8,1"。这样方便与其他串口进行通讯。

   GetSettings:取得串口参数。

   SetPortOpen:打开或关闭串口,当一个程序打开串口时,另外的程序将无法使用该串口。

   GetPortOpen:取得串口状态。

   GetInBufferCount:输入缓冲区中接受到的字符数。

   SetInPutLen:一次读取输入缓冲区的字符数。设置为0时,程序将读取缓冲区的全部字符。

   GetInPut:读取输入缓冲区。

   GetOutBufferCount:输出缓冲区中待发送的字符数。

   SetOutPut:写入输出缓冲区。

  一般而言,使用上述函数和属性就可以进行串口通讯了。以下是一个范例。

#define MESSAGELENGTH 100

class CMyDialog : public CDialog
{
protected: 
VARIANT InBuffer;
VARIANT OutBuffer;
CMSComm m_Com;
public:
......
}

BOOL CMyDiaLog::OnInitDialog()
{
CDialog::OnInitDialog();
m_Com.SetCommPort(1);
if (!m_Com.GetPortOpen()) {
m_Com.SetSettings("57600,N,8,1");
m_Com.SetPortOpen(true);
m_Com.SetInBufferCount(0);
SetTimer(1,10,NULL);
InBuffer.bstrVal=new unsigned short[MESSAGELENGTH];
OutBuffer.bstrVal=new unsigned short[MESSAGELENGTH];
OutBuffer.vt=VT_BSTR;
}
return true;
}

void CMyDiaLog::OnTimer(UINT nIDEvent) 
{
if (m_Com.GetInBufferCount()>=MESSAGELENGTH) {
InBuffer=m_Com.GetInput();
// handle the InBuffer. 
// Fill the OutBuffer. 
m_Com.SetOutput(OutBuffer);
}
CDialog::OnTimer(nIDEvent);
}

  用该控件传输的数据是UNICODE格式。关于UNICODE和ANSI的关系和转换请参看MSDN。 

  关于该控件的其他详细资料请查看MSDN关于COMM CONTROL部分。

二、直接用VC++访问串口。

  在VC++中,串口和磁盘文件可以统一的方式来简单读写。这两者几乎没有什么不同,只是在WINDOWS 9X下磁盘文件只能做同步访问,而串口只能做异步访问。

  CreateFile:用指定的方式打开指定的串口。通常的方式为

  m_hCom = CreateFile( "COM1", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL );

  m_hCom为文件句柄。GENERIC_READ | GENERIC_WRITE指定可以对串口进行读写操作。第三个参数0表示串口为独占打开。OPEN_EXISTING表示当指定串口不存在时,程序将返回失败。 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED则表示文件属性。当打开串口时,必须指定 FILE_FLAG_OVERLAPPED,它表示文件或设备不会维护访问指针,则在读写时,必须使用OVERLAPPED 结构指定访问的文件偏移量。

   ReadFile:读取串口数据。

   WriteFile:向串口写数据。

   CloseHandle:关闭串口。

  COMMTIMEOUTS:COMMTIMEOUTS主要用于串口超时参数设置。COMMTIMEOUTS结构如下:

typedef struct _COMMTIMEOUTS { 
DWORD ReadIntervalTimeout; 
DWORD ReadTotalTimeoutMultiplier; 
DWORD ReadTotalTimeoutConstant; 
DWORD WriteTotalTimeoutMultiplier; 
DWORD WriteTotalTimeoutConstant; 
} COMMTIMEOUTS,*LPCOMMTIMEOUTS; 

  ReadIntervalTimeout:两字符之间最大的延时,当读取串口数据时,一旦两个字符传输的时间差超过该时间,读取函数将返回现有的数据。设置为0表示该参数不起作用。 

  ReadTotalTimeoutMultiplier:读取每字符间的超时。

  ReadTotalTimeoutConstant:一次读取串口数据的固定超时。所以在一次读取串口的操作中,其超时为ReadTotalTimeoutMultiplier乘以读取的字节数再加上 ReadTotalTimeoutConstant。将ReadIntervalTimeout设置为MAXDWORD,并将ReadTotalTimeoutMultiplier 和ReadTotalTimeoutConstant设置为0,表示读取操作将立即返回存放在输入缓冲区的字符。

  WriteTotalTimeoutMultiplier:写入每字符间的超时。

  WriteTotalTimeoutConstant:一次写入串口数据的固定超时。所以在一次写入串口的操作中,其超时为WriteTotalTimeoutMultiplier乘以写入的字节数再加上 WriteTotalTimeoutConstant。

  SetCommTimeouts函数可以设置某设备句柄的超时参数,要得到某设备句柄的超时参数可以用GetCommTimeouts函数。

  DCB:DCB结构主要用于串口参数设置。该结构太庞大,这里就不一一讲述了,有兴趣者可查看MSDN关于DCB的描述。其中下面两个是比较重要的属性。

  BaudRate:串口的通讯速度。一般设置为9600。

  ByteSize:字节位数。一般设置为8。

  DCB结构可以用SetCommState函数来设置,并可以用GetCommState来得到现有串口的属性。

  SetupComm:设置串口输入、输出缓冲区。

  OVERLAPPED:保存串口异步通讯的信息。具体结构如下:

typedef struct _OVERLAPPED { 
DWORD Internal; 
DWORD InternalHigh; 
DWORD Offset; 
DWORD OffsetHigh; 
HANDLE hEvent; 
} OVERLAPPED; 

  Internal,InternalHigh是保留给系统使用的,用户不需要设置。

  Offset,OffsetHigh是读写串口的偏移量,一般设置OffsetHigh为NULL,可以支持2GB数据。

  hEvent读写事件,因为串口是异步通讯,操作可能被其他进程堵塞,程序可以通过检查该时间来得知是否读写完毕。事件将在读写完成后,自动设置为有效。

  通过以上这些函数和结构,我们就可以通过串口进行通讯了,现在我们具体看下面的实例:

BOOL CSerial::Open( int nPort, int nBaud )
{
if( m_bOpened ) return( TRUE );

char szPort[15];
DCB dcb;

wsprintf( szPort, "COM%d", nPort );
m_hComDev = CreateFile( szPort, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL );
if( m_hComDev == NULL ) return( FALSE );

memset( &m_OverlappedRead, 0, sizeof( OVERLAPPED ) );
memset( &m_OverlappedWrite, 0, sizeof( OVERLAPPED ) );

COMMTIMEOUTS CommTimeOuts;
CommTimeOuts.ReadIntervalTimeout = 0xFFFFFFFF;
CommTimeOuts.ReadTotalTimeoutMultiplier = 0;
CommTimeOuts.ReadTotalTimeoutConstant = 0;
CommTimeOuts.WriteTotalTimeoutMultiplier = 0;
CommTimeOuts.WriteTotalTimeoutConstant = 5000;
SetCommTimeouts( m_hComDev, &CommTimeOuts );

m_OverlappedRead.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
m_OverlappedWrite.hEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

dcb.DCBlength = sizeof( DCB );
GetCommState( m_hComDev, &dcb );
dcb.BaudRate = nBaud;
dcb.ByteSize = 8;
if( !SetCommState( m_hComDev, &dcb ) ||
!SetupComm( m_hComDev, 10000, 10000 ) ||
m_OverlappedRead.hEvent == NULL ||
m_OverlappedWrite.hEvent == NULL ){
DWORD dwError = GetLastError();
if( m_OverlappedRead.hEvent != NULL ) CloseHandle( m_OverlappedRead.hEvent );
if( m_OverlappedWrite.hEvent != NULL ) CloseHandle( m_OverlappedWrite.hEvent );
CloseHandle( m_hComDev );
return FALSE;
}

m_bOpened = TRUE;

return m_bOpened;

}

int CSerial::InBufferCount( void )
{

if( !m_bOpened || m_hComDev == NULL ) return( 0 );

DWORD dwErrorFlags;
COMSTAT ComStat;

ClearCommError( m_hIDComDev, &dwErrorFlags, &ComStat );

return (int)ComStat.cbInQue;

}

DWORD CSerial::ReadData( void *buffer, DWORD dwBytesRead)
{

if( !m_bOpened || m_hComDev == NULL ) return 0;

BOOL bReadStatus;
DWORD dwErrorFlags;
COMSTAT ComStat;

ClearCommError( m_hComDev, &dwErrorFlags, &ComStat );
if( !ComStat.cbInQue ) return 0;

dwBytesRead = min(dwBytesRead,(DWORD) ComStat.cbInQue);

bReadStatus = ReadFile( m_hComDev, buffer, dwBytesRead, &dwBytesRead, &m_OverlappedRead );
if( !bReadStatus ){
if( GetLastError() == ERROR_IO_PENDING ){
WaitForSingleObject( m_OverlappedRead.hEvent, 2000 );
return dwBytesRead;
}
return 0;
}

return dwBytesRead;

}

DWORD CSerial::SendData( const char *buffer, DWORD dwBytesWritten)
{

if( !m_bOpened || m_hComDev == NULL ) return( 0 );

BOOL bWriteStat;

bWriteStat = WriteFile( m_hComDev, buffer, dwBytesWritten, &dwBytesWritten, &m_OverlappedWrite );
if( !bWriteStat){
if ( GetLastError() == ERROR_IO_PENDING ) {
WaitForSingleObject( m_OverlappedWrite.hEvent, 1000 );
return dwBytesWritten;
}
return 0;
}
return dwBytesWritten;

}

  上述函数基本实现串口的打开,读写操作。本文章略去该串口类的说明和关闭函数。读者应该能将这些内容写完。接下来,你就可以在你的程序中调用该串口类了。关于本文有任何疑问,请与作者联系。

用vc++4.0实现win95socket编程
我们有时需要编制一些仅在后台监控的程序,为了不干扰前台程序的运行界面和不显示不必要的窗口,应使其运行时的主窗口不可见。同时,应该让用户知道该程序正在运行,并且达到与用户进行交互的目的。将一个图标显示在任务栏右端静态通告区中并响应用户的鼠标动作是当前非常流行的方法,它体现了Windows 95友好的界面风格。下面以一个SDI(单文档界面)程序为例,讲述采用Microsoft Visual C++ 5.0开发这类程序的主要步骤。 
  首先,要使程序的主窗口不可见,并且不在任务栏上出现任务按钮,要做到这两点,需分别设置主边框窗口的风格和扩展风格:

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
cs.style =WS_POPUP;//使主窗口不可见
cs.dwExStyle |=WS_EX_TOOLWINDOW;//不显示任务按钮
return CFrameWnd::PreCreateWindow(cs);
}

  其次,利用系统函数Shell_NotifyIcon将一个图标显示在 任务栏的通告区中。该函数的原型为:

WINSHELLAPI BOOL WINAPI Shell_NotifyIcon( DWORD dwMessage,PNOTIFYICONDATA pnid );

  下例中被显示的是主边框窗口的图标,实际上可以显示任何图标:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{



NOTIFYICONDATA tnd;
tnd.cbSize=sizeof(NOTIFYICONDATA);
tnd.hWnd=this-$#@62;m_hWnd;
tnd.uID=IDR_MAINFRAME;
tnd.uFlags=NIF_MESSAGE|NIF_ICON|NIF_TIP;
tnd.uCallbackMessage=WM_LIBEN;
tnd.hIcon=LoadIcon(AfxGetInstanceHandle(),MAKEINTRESOURCE(IDR_MAINFRAME));
strcpy(tnd.szTip,"提示信息");
Shell_NotifyIcon(NIM_ADD,&tnd);


}

  在调用该函数之前,需要确定其参数的取值,其中之一为一个具有NOTIFYICONDATA类型的结构。其原型为:

typedef struct _NOTIFYICONDATA { // nidWORD cbSize; HWND hWnd; UINT uID; UINT uFlags; UINT uCallbackMessage; HICON hIcon; charszTip[64]; }
NOTIFYICONDATA, *PNOTIFYICONDATA;

  在该结构的成员中,cbSize为该结? 占的字节数,hWnd为接受该图标所发出的消息的窗口的句柄,uID为被显示图标的ID,uFlags指明其余的几个成员(hIcon、uCallBackMessage和szTip)的值是否有效,uCallbackMessage为一个自定义的消息,当用户在该图标上作用一些鼠标动作时,将向hWnd成员中指定的窗口发出该消息,可以定义该消息为WM_USER+100。hIcon为被显示图标的句柄,szTip为一字符数组,当鼠标停留在该图标上时,将其内容显示在浮动的提示信息框中。Shell_NotifyIcon函数的另一个参数是一个预定义的消息,可以取如下值之一:NIM_ADD、NIM_DELETE或NIM_MODIFY,分别表示添加图标、删除图标 或修改图标。
  最后,要与用户进行交互,也就是当用户在该图标上单击或双击鼠标左键或右键时要执行相应的操作,至少也要响应用户终止该程序的意愿。上面已经提到,当用户在图标上进行鼠标动作时,将向hWnd成员中指定的窗口发出自定义的消息,该消息由uCallbackMessage成员指定(在上例中为 WM_LIBEN,取值为WM_USER+100)。因此,我们的任务就是在hWnd窗口中响应该自定义消息:

void CMainFrame::OnLiben(WPARAM wParam,LPARAM lParam)
{
UINT uID;//发出该消息的图标的ID
UINT uMouseMsg;//鼠标动作
POINT pt;
uID=(UINT) wParam;
uMouseMsg=(UINT) lParam;
if(uMouseMsg==WM_RBUTTONDOWN)//如果是单击右键

{
switch(uID)

{
case IDR_MAINFRAME://如果是我们的图标
GetCursorPos(&pt);//取得鼠标位置
…//执行相应操作
break;



default:

}

}

return; 
}

  需要注意的是,首先要在该窗口类的头文件中给出该消息映射函数的原型说明: 

afx_msg void OnLiben(WPARAM wParam,LPARAM lParam);

  并且要在CPP文件中的消息映射中加入相应的条目,注意一定要加在//{{AFX_MSG_MAP(CMainFrame)和//}}AFX_MSG_MAP之外:

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_COMMAND(ID_APP_EXIT, OnAppExit)
//}}AFX_MSG_MAP
ON_MESSAGE(WM_LIBEN,OnLiben)
END_MESSAGE_MAP()

  当程序结束时,需要删去通告区中的图标,这时同样应该调用Shell_NotifyIcon函数,只不过第一个参数是表示删除图标的NIM_DELETE 了:

void CMainFrame::OnAppExit() 
{
// TODO: Add your command handler code here
NOTIFYICONDATA tnid;
tnid.cbSize=sizeof(NOTIFYICONDATA);
tnid.hWnd=this-$#@62;m_hWnd;
tnid.uID=IDR_MAINFRAME;//保证删除的是我们的图标
Shell_NotifyIcon(NIM_DELETE,&tnid);
AfxPostQuitMessage(0);


用Winsock实现语音全双工通信使用

一、引言?
  Windows 95作为微机的操作系统,已经完全融入了网络与通信功能,不仅可以建立纯Windows 95环境下的“对等网络”,而且支持多种协议,如TCP/IP、IPX/SPX、NETBUI等。在TCP/IP协议组中,TPC是一种面向连接的协义,为用户提供可靠的、全双工的字节流服务,具有确认、流控制、多路复用和同步等功能,适于数据传输。UDP协议则是无连接的,每个分组都携带完整的目的地址,各分组在系统中独立传送。它不能保证分组的先后顺序,不进行分组出错的恢复与重传,因此不保证传输的可靠性,但是,它提供高传输效率的数据报服务,适于实时的语音、图像传输、广播消息等网络传输。?
  Winsock接口为进程间通信提供了一种新的手段,它不但能用于同一机器中的进程之间通信,而且支持网络通信功能。随着Windows 95的推出。Winsock已经被正式集成到了Windows系统中,同时包括了16位和32位的编程接口。而Winsock的开发工具也可以在Borland C++4.0、Visual C++2.0这些C编译器中找到,主要由一个名为winsock.h的头文件和动态连接库winsock.dll或wsodk32.dll组成,这两种动态连接库分别用于Win16和Win32的应用程序。?
  本文针对话音的全双工传输要求,采用UDP协议实现了实时网络通信。使用VisualC++2.0编译环境,其动态连接库名为wsock32.dll。?

二、主要函数的使用要点?
  通过建立双套接字,可以很方便地实现全双工网络通信。?
  1.套接字建立函数:?
SOCKET socket(int family,int type,int protocol)?
对于UDP协议,写为:?
SOCKRET s;?
s=socket(AF_INET,SOCK_DGRAM,0);?
或s=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)?
为了建立两个套接字,必须实现地址的重复绑定,即,当一个套接字已经绑定到某本地地址后,为了让另一个套接字重复使用该地址,必须为调用bind()函数绑定第二个套接字之前,通过函数setsockopt()为该套接字设置SO_REUSEADDR套接字选项。通过函数getsockopt()可获得套接字选项设置状态。需要注意的是,两个套接字所对应的端口号不能相同。?
此外,还涉及到套接字缓冲区的设置问题,按规定,每个区的设置范围是:不小于512个字节,大大于8k字节,根据需要,文中选用了4k字节。?

  2.套接字绑定函数?
  int bind(SOCKET s,struct sockaddr_in*name,int namelen)?
s是刚才创建好的套接字,name指向描述通讯对象的? 体的指针,namelen是该结构体的长度。该结构体中的分量包括:IP地址(对应name.sin_addr.s_addr)、端口号(name.sin_port)、地址类型(name.sin_family,一般都赋成AF_INET,表示是internet地址)。?
  (1)IP地址的填写方法:在全双工通信中,要把用户名对应的点分表示法地址转换成32位长整数格式的IP地址,使用inet_addr()函数。?
(2)端口号是用于表示同一台计算机不同的进程(应用程序),其分配方法有两种:1)进程可以让系统为套接字自动分配一端口号,只要在调用bind前将端口号指定为0即可。由系统自动分配的端口号位于1024~5000之间,而1~1023之间的任一TCP或UDP端口都是保留的,系统不允许任一进程使用保留端口,除非其有效用户ID是零(超级用户)。?
  (2)进程可为套接字指定一特定端口。这对于需要给套接字分配一众所端口的服务器是很有用的。指定范围为1024和65536之间。可任意指定。?
在本程序中,对两个套接字的端口号规定为2000和2001,前者对应发送套接字,后者对应接收套接字。?
端口号要从一个16位无符号数(u_short类型数)从主机字节顺序转换成网络字节顺序,使用
htons()函数。?
根据以上两个函数,可以给出双套接字建立与绑定的程序片断;?
//设置有关的全局变量?
SOCKET sr,ss;?
HPSTR sockBufferS,sockBufferR;?
HANDLE hSendData,hReceiveData;?
DWROD dwDataSize=1024*4;?
struct sockaddr_in therel.there2;?
#DEFINE LOCAL_HOST_ADDR 200.200.200.201?
#DEFINE REMOTE_HOST-ADDR 200.200.200.202?
#DEFINE LOCAL_HOST_PORT 2000?
#DEFINE LOCAL_HOST_PORT 2001?
//套接字建立函数?
BOOL make_skt(HWND hwnd)?
{?

struct sockaddr_in here,here1;?
ss=socket(AF_INET,SOCK_DGRAM,0);?
sr=socket(AF_INET,SOCK_DGRAM,0);?
if((ss==INVALID_SOCKET)||(sr==INVALID_SOCKET))?
{?

MessageBox(hwnd,“套接字建立失败!”,“”,MB_OK);?
return(FALSE);?

}?
here.sin_family=AF_INET;?
here.sin_addr.s_addr=inet_addr(LOCAL_HOST_ADDR);?
here.sin_port=htons(LICAL_HOST_PORT);?
//another socket?
herel.sin_family=AF_INET;?
herel.sin_addr.s_addr(LOCAL_HOST_ADDR);?
herel.sin_port=htons(LOCAL_HOST_PORT1);?
SocketBuffer();//套接字缓冲区的锁定设置?
setsockopt(ss,SOL_SOCKET,SO_SNDBUF,(char FAR*)sockBufferS,dwDataSize);
if(bind(ss,(LPSOCKADDR)&here,sizeof(here)))
{

MessageBox(hwnd,“发送套接字绑定失败!”,“”,MB_OK);
return(FALSE);

}
setsockopt(sr SQL_SOCKET,SO_RCVBUF|SO_REUSEADDR,(char FAR*)
sockBufferR,dwDataSize);
if(bind(sr,(LPSOCKADDR)&here1,sizeof(here1)))
{

MessageBox(hwnd,“接收套接字绑定失败!”,“”,MB_OK);
return(FALSE);

}
return(TRUE);

}
//套接字缓冲区设置
void sockBuffer(void)
{

hSendData=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize);
if(!hSendData)
{

MessageBox(hwnd,“发送套接字缓冲区定位失败!”,NULL,
MB_OK|MB_ICONEXCLAMATION);
return;

}
if((sockBufferS=GlobalLock(hSendData)==NULL)
{

MessageBox(hwnd,“发送套接字缓冲区锁定失败!”,NULL,
MB_OK|MB_ICONEXCLAMATION);
GlobalFree(hRecordData[0];
return;

}
hReceiveData=globalAlloc(GMEM_MOVEABLE|GMEM_SHARE,dwDataSize);
if(!hReceiveData)
{

MessageBox(hwnd,"“接收套接字缓冲区定位败!”,NULL
MB_OK|MB_ICONEXCLAMATION);
return;

}
if((sockBufferT=Globallock(hReceiveData))=NULL)
MessageBox(hwnd,"发送套接字缓冲区锁定失败!”,NULL,
MB_OK|MB_ICONEXCLAMATION);
GlobalFree(hRecordData[0]);
return;

}

  3.数据发送与接收函数;?

  • int sendto(SOCKET s.char*buf,int len,int flags,struct sockaddr_in to,int
    tolen);?

  • int recvfrom(SOCKET s.char*buf,int len,int flags,struct sockaddr_in 
    fron,int*fromlen)其中,参数flags一般取0。?

  recvfrom()函数实际上是读取sendto()函数发过来的一个数据包,当读到的数据字节少于规定接收的数目时,就把数据全部接收,并返回实际接收到的字节数;当读到的数据多于规定值时,在数据报文方式下,多余的数据将被丢弃。而在流方式下,剩余的数据由下recvfrom()读出。为了发送和接收数据,必须建立数据发送缓冲区和数据接收缓冲区。规定:IP层的一个数据报最大不超过64K(含数据报头)。当缓冲区设置得过多、过大时,常因内存不够而导致套接字建立失败。在减小缓冲区后,该错误消失。经过实验,文中选用了4K字节。?
此外,还应注意这两个函数中最后参数的写法,给sendto()的最后参数是一个整数值,而recvfrom()的则是指向一整数值的指针。?
4.套接字关闭函数:closesocket(SOCKET s)?
通讯结束时,应关闭指定的套接字,以释与之相关的资源。?
在关闭套接字时,应先对锁定的各种缓冲区加以释放。其程序片断为:?
void CloseSocket(void)?
{?

GlobalUnlock(hSendData);?
GlobalFree(hSenddata);?
GlobalUnlock(hReceiveData);?
GlobalFree(hReceiveDava);?
if(WSAAysncSelect(ss,hwnd,0,0)=SOCKET_ERROR)?
{?

MessageBos(hwnd,“发送套接字关闭失败!”,“”,MB_OK);?
return;?

}?
if(WSAAysncSelect(sr,hwnd,0,0)==SOCKET_ERROR)?
{?

MessageBox(hwnd,“接收套接字关闭失败!”,“”,MB_OK);?
return;?
}?
WSACleanup();?
closesockent(ss);?
closesockent(sr);?
return;?

}

  三、Winsock的编程特点与异步选择机制?
  1 阻塞及其处理方式?
  在网络通讯中,由于网络拥挤或一次发送的数据量过大等原因,经常会发生交换的数据在短时间内不能传送完,收发数据的函数因此不能返回,这种现象叫做阻塞。Winsock对有可能阻塞的函数提供了两种处理方式:阻塞和非阻塞方式。在阻塞方式下,收发数据的函数在被调用后一直要到传送完毕或者出错才能返回。在阻塞期间,被阻的函数不会断调用系统函数GetMessage()来保持消息循环的正常进行。对于非阻塞方式,函数被调用后立即返回,当传送完成后由Winsock给程序发一个事先约定好的消息。?
在编程时,应尽量使用非阻塞方式。因为在阻塞方式下,用户可能会长时间的等待过程中试图关闭程序,因为消息循环还在起作用,所以程序的窗口可能被关闭,这样当函数从Winsock的动态连接库中返回时,主程序已经从内存中删除,这显然是极其危险的。?

  2 异步选择函数WSAAsyncSelect()的使用?
  Winsock通过WSAAsyncSelect()自动地设置套接字处于非阻塞方式。使用Windows Sockets实现Windows网络程序设计的关键就是它提供了对网络事件基于消息的异步存取,用于注册应用程序感兴趣的网络事件。它请求Windows Sockets DLL在检测到套接字上发生的网络事件时,向窗口发送一个消息。对UDP协议,这些网络事件主要为:?

  • FD_READ 期望在套接字收到数据(即读准备好)时接收通知;?
  • FD_WRITE 期望在套接字可发送数(即写准备好)时接收通知;?
  • FD_CLOSE 期望在套接字关闭时接电通知?

  消息变量wParam指示发生网络事件的套接字,变量1Param的低字节描述发生的网络事件,高字包含错误码。如在窗口函数的消息循环中均加一个分支:?
int ok=sizeof(SOCKADDR);?
case wMsg;?
switch(1Param)?
{?

case FD_READ:?
//套接字上读数据?
if(recvfrom(sr.lpPlayData[j],dwDataSize,0,(struct sockaddr FAR*)&there1,

(int FAR*)&ok)==SOCKET_ERROR0?
{?

MessageBox)hwnd,“数据接收失败!”,“”,MB_OK);?
return(FALSE);?

}?
case FD_WRITE:?
//套接字上写数据?

}?
break;?
  在程序的编制中,应根据需要灵活地将WSAAsyncSelect()函灵敏放在相应的消息循环之中,其它说明可参见文献[1]。此外,应该指出的是,以上程序片断中的消息框主要是为程序调试方便而设置的,而在正式产品中不再出现。同时,按照程序容错误设计,应建立一个专门的容错处理函数。程序中可能出现的各种错误都将由该函数进行处理,依据错误的危害程度不同,建立几种不同的处理措施。这样,才能保证双方通话的顺利和可靠。?

四、结论?
  本文是多媒体网络传输项目的重要内容之一,目前,结合硬件全双工语音卡等设备,已经成功地实现了话音的全双工的通信。有关整个多媒体传输系统设计的内容,将有另文叙述。

原创粉丝点击