songyujin

导航

socket 编程

对于一个程序员而言,学习一种语言和一种算法是非常容易的(不包括那些上学花很多时间玩,上班说学习没时间的人)。但是,任何程序都可能是有瑕疵的,尤其有过团队协作编程经验的人,对这个感触尤为深刻。

 

在我前面的述及调试的文章里,我侧重于VC集成环境中的一些设置信息和调试所需要的一些基本技巧。但是,仅仅知道这些是不够的。一个成功的调试的开端是编程中的准备。

 

分离错误

很多程序员喜欢写下面这样的式子:

   CLeftView* pView =
((CFrameWnd*)AfxGetApp()->m_pMainWnd)->m_wndSplitterWnd.GetPane(0,0);

如果一切顺利,这样的式子当然是没什么问题。但是作为一个程序员,你应该时刻记得任何一个调用在某些特殊的情况下都可能失败,一旦上面某个式子失败,那么整个级联式就会出问题,而你很难弄清楚到底哪儿出错了。这样的式子的结果往往是:省了2分钟编码的时间,多了几星期的调试时间。

对于上面的式子,应该尽可能的把式子分解成独立的函数调用,这样我们可以随时确定是哪个函数调用出问题,进口缩小需要检查的范围。

检查返回值

检查返回值对于许多编程者来说似乎是一个很麻烦的事情。但是如果你能在每个可能出错的函数调用处都检查返回值,就可以立刻知道出错的函数。

有些人已经意识到检查返回值的重要性,但是要记住,只检查函数是否失败是不够的,我们需要知道函数失败的确切原因。例如下面的代码:

if(connect(sock, (const sockaddr*)&addr,sizeof(addr)) == SOCKET_ERROR)
{
AfxMessageBox("connect failed");
}

尽管这里已经检查了返回值,实际上没有多少帮助。正如很多在vckbase上提问的人一样,大概这时候只能喊“为什么连接失败啊?”。这种情况下,其实只能猜测失败的原因,即使高手,也无法准确说出失败的原因。

增加诊断信息

在知道错误的情况下,应该尽可能的告诉测试、使用者更多的信息,这样才能了解导致失败的原因。如果程序员能提供如下错误信息,对于诊断错误是非常有帮助的:

  1. 出错的文件:我们可以借助宏THIS_FILE和__FILE__。注意THIS_FILE是在cpp文件手工定义的,而__FILE__是编译器定义的。当记录错误的函数定义在.h中时,有时候用THIS_FILE更好,因为他能说明在哪个cpp中调用并导致失败的。
  2. 出错的行:我们可以借助宏__LINE__
  3. 出错的函数:如果设计的好,有以上两项已经足够。当然我们可以直接打印出出错的函数或者表达式,这样在大堆代码中搜索(尤其是不支持go to line的编辑器中)还是很有用的。大家可以参见我的文章http://blog.vckbase.com/arong/archive/2005/11/10/14704.html中的方式进行处理,也许是一个基本的开端。
  4. 出错的原因:出错的原因很多只能由程序自己给出。如果出错只会问别人,那么你永远不可能成为一个合格的程序设计人员。很多函数失败时都会设置errno。我们可以用GetLastError获得错误码,并通过FormatMessage打印出具体错误的文字描述。

终了

给初学者一个忠告:编程时麻烦10分钟,调试时省却数小时,要想省时间,还是要从代码的可重用性和可维护性上下功夫,而不是两个代码上节省。
发表于 2005-12-07 22:12 馨荣家园 阅读(511) | 评论 (4)编辑 收藏

2005年12月3日 #

现象

用多线程方法设计socket程序时,你会发现在跨线程使用CAsyncSocket及其派生类时,会出现程序崩溃。所谓跨线程,是指该对象在一个线程中调用Create/AttachHandle/Attach函数,然后在另外一个线程中调用其他成员函数。下面的例子就是一个典型的导致崩溃的过程:
CAsyncSocket Socket;
UINT Thread(LPVOID)
{
Socket.Close ();
return 0;
}
void CTestSDlg::OnOK()
{
// TODO: Add extra validation here
Socket.Create(0);
AfxBeginThread(Thread,0,0,0,0,0);
}

其中Socket对象在主线程中被调用,在子线程中被关闭。

跟踪分析

这个问题的原因可以通过单步跟踪(F11)的方法来了解。我们在Socket.Create(0)处设断点,跟踪进去会发现下面的函数被调用:

void PASCAL CAsyncSocket::AttachHandle(
SOCKET hSocket, CAsyncSocket* pSocket, BOOL bDead)
{
_AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
BOOL bEnable = AfxEnableMemoryTracking(FALSE);
if (!bDead)
{
ASSERT(CAsyncSocket::LookupHandle(hSocket, bDead) == NULL);
if (pState->m_pmapSocketHandle->IsEmpty())
{
ASSERT(pState->m_pmapDeadSockets->IsEmpty());
ASSERT(pState->m_hSocketWindow == NULL);
CSocketWnd* pWnd = new CSocketWnd;
pWnd->m_hWnd = NULL;
if (!pWnd->CreateEx(0, AfxRegisterWndClass(0),
_T("Socket Notification Sink"),
WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL))
{
TRACE0("Warning: unable to create socket notify window!\n");
AfxThrowResourceException();
}
ASSERT(pWnd->m_hWnd != NULL);
ASSERT(CWnd::FromHandlePermanent(pWnd->m_hWnd) == pWnd);
pState->m_hSocketWindow = pWnd->m_hWnd;
}
pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
}
else
{
int nCount;
if (pState->m_pmapDeadSockets->Lookup((void*)hSocket, (void*&)nCount))
nCount++;
else
nCount = 1;
pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
}
AfxEnableMemoryTracking(bEnable);
}

在这个函数的开头,首先获得了一个pState的指针指向_afxSockThreadState对象。从名字可以看出,这似乎是一个和线程相关的变量,实际上它是一个宏,定义如下:

#define _afxSockThreadState AfxGetModuleThreadState()

我们没有必要去细究这个指针的定义是如何的,只要知道它是和当前线程密切关联的,其他线程应该也有类似的指针,只是指向不同的结构。

在这个函数中,CAsyncSocket创建了一个窗口,并把如下两个信息加入到pState所管理的结构中:

    pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);
pState->m_pmapDeadSockets->SetAt((void*)hSocket, (void*)nCount);
pState->m_hSocketWindow = pWnd->m_hWnd;
pState->m_pmapSocketHandle->SetAt((void*)hSocket, pSocket);

当调用Close时,我们再次跟踪,就会发现在KillSocket中,下面的函数出现错误:

    void PASCAL CAsyncSocket::KillSocket(SOCKET hSocket, CAsyncSocket* pSocket)
{
ASSERT(CAsyncSocket::LookupHandle(hSocket, FALSE) != NULL);

我们在这个ASSERT处设置断点,跟踪进LookupHandle,会发现这个函数定义如下:

CAsyncSocket* PASCAL CAsyncSocket::LookupHandle(SOCKET hSocket, BOOL bDead)
{
CAsyncSocket* pSocket;
_AFX_SOCK_THREAD_STATE* pState = _afxSockThreadState;
if (!bDead)
{
pSocket = (CAsyncSocket*)
pState->m_pmapSocketHandle->GetValueAt((void*)hSocket);
if (pSocket != NULL)
return pSocket;
}
else
{
pSocket = (CAsyncSocket*)
pState->m_pmapDeadSockets->GetValueAt((void*)hSocket);
if (pSocket != NULL)
return pSocket;
}
return NULL;
}

显然,这个函数试图从当前线程查询关于这个 socket的信息,可是这个信息放在创建这个socket的线程中,因此这种查询显然会失败,最终返回NULL。

有人会问,既然它是ASSERT出错,是不是Release就没问题了。这只是自欺欺人。ASSERT/VERIFY都是检验一些程序正常运行必须正确的条件。如果ASSERT都失败,在Release中也许不会显现,但是你的程序肯定运行不正确,啥时候出错就不知道了。

如何在多线程之间传递socket

有些特殊情况下,可能需要在不同线程之间传递socket。当然我不建议在使用CAsyncSOcket的时候这么做,因为这增加了出错的风险(尤其当出现拆解包问题时,有人称为粘包,我基本不认同这种称呼)。如果一定要这么做,方法应该是:

  1. 当前拥有这个socket的线程调用Detach方法,这样socket句柄和C++对象及当前线程脱离关系
  2. 当前线程把这个对象传递给另外一个线程
  3. 另外一个线程创建新的CAsyncSocket对象,并调用Attach

上面的例子,我稍微做修改,就不会出错了:

CAsyncSocket Socket;
UINT Thread(LPVOID sock)
{
Socket.Attach((SOCKET)sock);
Socket.Close ();
return 0;
}
void CTestSDlg::OnOK()
{
// TODO: Add extra validation here
Socket.Create(0);
SOCKET hSocket = Socket.Detach ();
AfxBeginThread(Thread,(LPVOID)hSocket,0,0,0,0);
}
发表于 2005-12-03 14:08 馨荣家园 阅读(502) | 评论 (3)编辑 收藏

2005年11月20日 #

++运算符和--运算符最早出现的原因是他是单指令的,因此效率很高,考虑
x = x+1
和x++
前者需要两个mov指令和一个add指令,而后者只需要一个inc指令,在效率至上年代,这是很重要的。

但是,现在的软件都很大,一两个指令的优化对程序基本上没有任何意义。而程序的可维护性和可读性则在大软件中更为重要。由于++运算符和--运算符在复杂表达式中往往受运算顺序影响,其结果是无法预知的,因此现代软件工程往往对这两个指令持反对态度。

考虑常见的国内垃圾考试题,计算a= (b++) + (b--) 的值,这种题目到底结果如何,老实说,即使知道也没有任何意义,因为你根本不应该写出这样的式子。合理的式子应该是:

a = (b+1) + (b-1);
b++;

建议尤其是新学生,把不用++或者--当作一个教条,这样对你只有好处,没有害处。如果一定要用,必须保证一个表达式就是一个变量和++或者--的结合,也就是只能使用如下四种形式:

a++;
a--;
++a;
--a;
发表于 2005-11-20 15:38 馨荣家园 阅读(612) | 评论 (8)编辑 收藏

2005年11月10日 #

根据前面一节的说明,服务端套接字应该按照如下顺序建立:
  1. 初始化
  2. 创建套接字
  3. 绑定本地地址
  4. 进入侦听状态
  5. 处理接受循环

下面首先创建一个例子来演示服务端套接字的实现,并在以后的各节中优化这个设计。

这个设计实现的功能如下:允许客户端(实际上就是telnet程序)登陆,并对客户端的输入回显。

4.1 准备工程

为了实现这个demo,我打算使用Visual Studio 6.0提供的对话框模板来实现,请按照下述步骤准备工程:

  1. 启动Visual C++ 6.0
  2. 选择File/New/Project
  3. 选择MFC AppWizard(exe)
  4. 在Project name中输入demo1, Next
  5. 选择Dialog Base, Next
  6. 在Windows Sockets前打钩
  7. 其他都保持缺省值,点击Next完成向导
  8. 在工程中加入两个文件:sockutil.h和sockutil.cpp:这两个文件将保存公共的函数,避免以后重复编写。

这样创建的工程会自动加入WinSock2支持,具体的内容包括:

  1. 在stdafx.h中加入#include <afxsock.h>,该头文件包含如下语句载入对应LIB:
    #pragma comment(lib, "wsock32.lib")
  2. 在InitInstance中加入如下代码:
    if(!AfxSocketInit())
        {
        AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
        return FALSE;
        }
        
  3. 在资源中定义字符串资源IDP_SOCKETS_INIT_FAILED:Windows通讯端口初始化失败

    如果大家创建工程时没有加入对应的sock支持,我们可以参照上述列表手工加入。

    对于前文所述的sockutil.h文件,加入如下初始代码:

    #if !defined(__SOCKET_UTILITY_HEADER__)
        #define __SOCKET_UTILITY_HEADER__
        #if _MSC_VER > 1000
        #pragma once
        #endif // _MSC_VER > 1000
        #endif
        

    对于sockutil.cpp文件,则加入如下初始代码:

    #include "stdafx.h"
        #include "sockutil.h"
        #ifdef _DEBUG
        #define new DEBUG_NEW
        #undef THIS_FILE
        static char THIS_FILE[] = __FILE__;
        #endif
        

    4.2 准备错误处理方式

    在编制每个程序之前,我习惯是为错误处理定制一个统一的处理方式。在以后的例子中,我会按照如下的方式处理错误:在一个log文件中记录错误发生的位置(文件、行号),错误号和字符串解释。为了实现这个目的,我在sockutil.cpp定义如下的函数:

    bool ErrorHandle(LPCTSTR expression, bool bFalse, LPCTSTR file, UINT line)
        {
        if(!bFalse)
        {//没有错误,直接返回
        return false;
        }
        FILE * fp;
        fp = fopen("demo.log","at");
        if(NULL == fp)
        {//如果文件打开失败,放弃记录
        return true;
        }
        //获得错误码
        DWORD ErrorCode = GetLastError();
        //格式化成字符串格式
        LPVOID lpMsgBuf;
        FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        ErrorCode ,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
        (LPTSTR) &lpMsgBuf,
        0,
        NULL
        );
        //输出到文件
        fprintf(fp,_T("file=%s,line=%u\n%d:%s\n"),file,line,ErrorCode, (LPCTSTR)lpMsgBuf);
        // 释放空间,该空间由FormatMessage分配
        LocalFree( lpMsgBuf );
        //关闭文件
        fclose(fp);
        return true;
        }
        

    在sockutil.h中,添加如下代码:

    bool ErrorHandle(LPCTSTR expression, bool bFalse, LPCTSTR file, UINT line);
        #define ERRORHANDLE(expression) ErrorHandle(#expression,(expression),__FILE__,__LINE__)
        

    4.3 核心代码

    根据前文,设计该服务端套接字主函数如下:

    void sockmain(LPCTSTR ip, UINT port)
        {
        SOCKET hSocket;
        hSocket = socket(AF_INET,SOCK_STREAM,0);
        if(ERRORHANDLE(hSocket == INVALID_SOCKET))
        {
        return;
        }
        sockaddr_in addr;
        InitializeAddress(inet_addr(ip), port, addr);
        if(ERRORHANDLE(SOCKET_ERROR == bind(hSocket, (const sockaddr*) & addr, sizeof(addr))))
        {
        closesocket(hSocket);
        return;
        }
        if(ERRORHANDLE(SOCKET_ERROR == listen(hSocket,5)))
        {
        closesocket(hSocket);
        return;
        }
        SOCKET hClient;
        int size;
        char buffer[2048];
        int length;
        size = sizeof(addr);
        while(INVALID_SOCKET != (hClient = accept(hSocket,(sockaddr*)&addr, & size)))
        {
        size = sizeof(addr);
        while((length = recv(hClient, buffer, sizeof(buffer),0)) > 0)
        {
        SendData(hClient,buffer, length);
        }
        closesocket(hClient);
        }
        closesocket(hSocket);
        return;
        }
        

    其中,InitializeAddress定义如下:

    void InitializeAddress(DWORD ip, UINT port, sockaddr_in & addr)
        {
        memset(&addr,0,sizeof(addr));
        addr.sin_family  = AF_INET;
        addr.sin_addr.s_addr= ip;
        addr.sin_port       = htons(port);
        }
        
    SendData定义如下:
    int SendData(SOCKET hSocket, const char * data, int length)
        {
        int result;
        int pos = 0;
        while(pos < length)
        {
        result = send(hSocket, data + pos, length - pos , 0);
        if(result > 0 )
        {
        pos += result;
        }else{
        return result;
        }
        }
        return length;
        }
        

    4.4 启动该任务

    为了启动服务端套接字,我们可以在对话框资源的OK按钮上双击,然后在OnOK中添加如下代码

       sockmain("0.0.0.0",2000);
        

    当我们启动该工程,并点击OK按钮,就可以通过telnet来测试是否有回显功能了。

    发表于 2005-11-10 15:04 馨荣家园 阅读(594) | 评论 (3)编辑 收藏

    2005年11月4日 #

    sockutil.cpp

    #include "stdafx.h"
        #include <iostream.h>
        #include <winsock2.h>
        #include "sockutil.h"
        void ShowError(unsigned int nError)
        {
        void* lpMsgBuf;
        FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM,
        NULL,
        nError,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );
        cout <<"(" << nError  << "):" << lpMsgBuf;
        LocalFree(lpMsgBuf);
        }
        BOOL ErrorHandle(const char * position, BOOL condition, const char * file, unsigned int line)
        {
        if(!condition)
        {
        return condition;
        }
        cout <<file << "(" << line << ")" << endl;
        cout <<position<< endl;
        unsigned int nError = GetLastError();
        ShowError(nError);
        return condition;
        }
        void InitializeAddress(DWORD ip, UINT port, sockaddr_in & addr)
        {
        memset(&addr,0,sizeof(addr));
        addr.sin_family  = AF_INET;
        addr.sin_addr.s_addr= ip;
        addr.sin_port       = htons(port);
        }
        int SendData(SOCKET hSocket, const char * data, int length)
        {
        int result;
        int pos = 0;
        while(pos < length)
        {
        result = send(hSocket, data + pos, length - pos , 0);
        if(result > 0 )
        {
        pos += result;
        }else{
        return result;
        }
        }
        return length;
        }
        

    demo1.cpp

    // demo1.cpp : Defines the entry point for the console application.
        //
        #include "stdafx.h"
        #include <stdio.h>
        #include <winsock2.h>
        #pragma comment(lib,"ws2_32")
        #include "sockutil.h"
        int main(int argc, char* argv[])
        {
        unsigned short wVersion;
        WSADATA wsa;
        wVersion = MAKEWORD(2,2);
        if(ERRORHANDLE(WSAStartup(wVersion, &wsa) != 0))
        {
        return -1;
        }
        SOCKET hSocket;
        int a = PROTO_ICMP;
        hSocket = socket(AF_INET,SOCK_STREAM,0);
        if(ERRORHANDLE(hSocket == INVALID_SOCKET))
        {
        WSACleanup();
        return -1;
        }
        sockaddr_in addr;
        InitializeAddress(INADDR_ANY, 2000, addr);
        if(ERRORHANDLE(SOCKET_ERROR == bind(hSocket, (const sockaddr*) & addr, sizeof(addr))))
        {
        closesocket(hSocket);
        WSACleanup();
        return -1;
        }
        if(ERRORHANDLE(SOCKET_ERROR == listen(hSocket,5)))
        {
        closesocket(hSocket);
        WSACleanup();
        return -1;
        }
        SOCKET hClient;
        int size;
        char buffer[2048];
        int length;
        size = sizeof(addr);
        while(INVALID_SOCKET != (hClient = accept(hSocket,(sockaddr*)&addr, & size)))
        {
        size = sizeof(addr);
        while((length = recv(hClient, buffer, sizeof(buffer),0)) > 0)
        {
        SendData(hClient,buffer, length);
        }
        closesocket(hClient);
        }
        closesocket(hSocket);
        WSACleanup();
        return 0;
        }
        
    发表于 2005-11-04 19:46 馨荣家园 阅读(346) | 评论 (0)编辑 收藏

    2005年10月31日 #

    服务端套接字是编程最简单的一个部分,甚至在各种环境下都可以用类似的代码。对于一个服务端套接字而言,他的基本工作包括:

    1. 初始化
    2. 创建套接字
    3. 给套接字捆绑一个本地地址
    4. 【可选】设置套接字属性
    5. 调用listen函数
    6. 进入accept循环,接受来自客户端的请求
    7. 对PCS进行管理
    8. 释放服务端套接字所占用的资源

    3.1 初始化

    对于每个需要处理套接字的程序,在调用任何其他套接字函数之前,必须首先调用WSAStartup函数。如果不调用它,其他函数调用会失败,并返回WSANOTINITIALISED错误码,表示WinSock库还没有初始化。对于除了其他操作系统,初始化的方法会不一样,请参见对应的文档找到初始化方法。

    WSAStartup的函数原型为:

    int WSAStartup(
        WORD wVersionRequested,
        LPWSADATA lpWSAData
        );

    其中,各个参数的含义如下:

    wVersionRequested: 本程序所需要的最低版本号。请注意,MSDN中所说的是本程序可能用到的最高版本的函数,其含义和我所说的一样。
    lpWSAData 返回WSADATA结构,说明WinSock库当前的实现细节

    该函数成功时返回0,否则可能返回如下一些错误码:

    WSASYSNOTREADY 底层网络系统未准备好
    WSAVERNOTSUPPORTED 所要求版本号本实现不支持
    WSAEINPROGRESS 一个阻塞性套接字操作未完成
    WSAEPROCLIM 达到当前实现所支持的最大任务数
    WSAEFAULT lpWSAData是一个非法指针

    一般用户并不关心实现细节,只要当前实现库满足最低版本需求即可。常见代码为:

     WORD wVersion = MAKEWORD(2,2);//最低版本2.2
    WSADATA WSAData; int nResult; if(ERROR_SUCCESS != (nResult = WSAStartup(wVersion,&WSAData))) { ReportError("WSAStartup", nResult); return -1; }

    3.2 创建套接字

    每个套接字任务都从创建套接字开始。我们可以用socket函数来创建套接字,该函数的原型为:

    SOCKET socket(
                            int af,
                            int type,
                            int protocol
                            )
                            
    其中,各个参数的含义为: 协议号,说明该套接字所处理的协议。他的可选值随前面两个参数不同而不同。似乎在RAW协议中用的比较多,大家可以在ROUTPROT.h中找到类似定义。
    af 地址族,说明该socket支持的地址类型。我们可以在winsock2.h中找到所支持的地址族。不过一般来说,对于TCP/IP编程,我们都会设置为AF_INET
    type 协议类型,Winsock2.h中列出了5种类型,我们一般会使用其中的三种,SOCK_STREAM表示流协议,SOCK_DGRAM表示数据报协议,SOCK_RAW表示原始套接字。我会在数据传输部分详细解释这些内容
    protocol

    用于侦听的套接字需要是流套接字,下面代码会创建这样的套接字:

        SOCKET hSocket;
                                    int a = PROTO_ICMP;
                                    hSocket = socket(AF_INET,SOCK_STREAM,0);
                                    if(ERRORHANDLE(hSocket == INVALID_SOCKET))
                                    {
                                    WSACleanup();
                                    return -1;
                                    }
                                    

    3.3 捆绑本地地址

    每个套接字必须有一个地址才能和对方通讯。bind函数用于捆绑地址,它的函数原型为:

    int bind(
                                    SOCKET s,
                                    const struct sockaddr* name,
                                    int namelen
                                    );
                                    

    各个参数含义如下:

    s 套接字号
    name 地址信息:需要注意的是,套接字接口是给多种协议共享的,sockaddr结构只是一个占位符,不同协议使用不同的地址结构,例如,TCP/IP编程使用的结构是sockaddr_in
    namelen 地址结构的长度,加入这个参数的原因也就是因为不同协议有不同的结构

    需要注意的,bind函数只能给socket绑定本机的IP地址,如果你给出的地址信息是其他机器的,则必然会失败。此时该函数返回SOCKET_ERROR,WSAGetLastError则返回WSAEADDRNOTAVAIL。

    如果没有特殊需求,应该设置该结构的IP地址为INADDR_ANY。对于端口,服务端套接字需要指定一个端口,而客户端端口最好设置为0,让系统选择一个可用端口。

    下面代码初始化一个地址结构:

    void InitializeAddress(DWORD ip, UINT port, sockaddr_in & addr)
    {
    memset(&addr,0,sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr= ip;
    addr.sin_port = htons(port);
    }

    下面代码则把一个套接字绑定到本机2000端口上:

    if(ERRORHANDLE(SOCKET_ERROR == bind(hSocket, (const sockaddr*) & addr, sizeof(addr)))) 
    {
    closesocket(hSocket);
    WSACleanup();
    return -1;
    }

    3.4 进入侦听状态

    当我们创建一个套接字,并绑定了地址后,我们需要设置这个套接字进入侦听状态,进入侦听状态后,该套接字就可以处理来自客户端的链接请求。

    listen函数设置套接字进入侦听状态,其函数原型为:

    int listen(
                                            SOCKET s,
                                            int backlog
                                            );

    对于刚开始编程的人,最容易误解的是backlog参数。许多人以为这就是该套接字最多能接收的链接的数目。实际上,一个套接字能接受的链接的数目不受这个参数控制,它只受系统资源的限制。例如对于linux,套接字用文件句柄实现,那么它可能受最大文件句柄数的限制。

    这个参数的含义是最多未决连接的数目,也就是连接请求已经到了服务端套接字,但是用户还没有调用accept的套接字数目。对于WinSock2,这个参数最大值为5。

    下面示例代码说明了如何调用listen:

    if(ERRORHANDLE(SOCKET_ERROR == listen(hSocket,5)))
                                            {
                                            closesocket(hSocket);
                                            WSACleanup();
                                            return -1;
                                            }

    3.5 accept循环

    当一个套接字处于侦听状态以后,我们就可以循环调用accept来接受新连接。accept函数的原型如下:

    SOCKET accept(
                                            SOCKET s,
                                            struct sockaddr* addr,
                                            int* addrlen
                                            );
                                            

    这个函数的参数说明和前面bind的一样。需要说明的是,在MSDN中说后面两个参数都是out参数,经过我的测试,结论并不一样。对于addrlen参数,应该是一个in/out参数,也就是说,如果第二个参数是一个结构指针,则第三个参数必须是一个整型变量的指针,该整型变量还必须被设置为该结构的长度。

    3.6 PCS管理

    由于一个一个服务端套接字可能接收无数个PCS,如何管理这些PCS就成为一个问题。不同的程序员有自己不同的管理方式,在此我就不准备细讲了。

    发表于 2005-10-31 20:14 馨荣家园 阅读(533) | 评论 (0)编辑 收藏

    2005年10月6日 #

    1。我这个人缺点就是不爱看书,不愿意学习先进知识。因此这里的东西都是经验总结而已,有可能有错误或者比较落后。建议有高追求的读者在看完后看看ACE等成熟的平台
    2。本人提供的代码都是简单测试即写出,因此不保证在您的系统中能够运行,也不提供后续的调试支持。
    3。由于我个人也比较忙,因此可能提供文章的过程不够连续,所以希望大家耐心。

    如果其中有任何错误,请把错误信息发送给arongustc@hotmail.com。在本站留言可能会被我忽略。但是请不要发邮件去讨论您编程中的问题,因为实在没有时间回复各位。

    谢谢。
    发表于 2005-10-06 11:52 馨荣家园 阅读(903) | 评论 (4)编辑 收藏

    说明 前一篇


    所有的WinSock函数都使用sockaddr结构来传递地址信息,该结构定义如下:

    struct sockaddr {
    u_short sa_family; /* address family */ char sa_data[14];/* up to 14 bytes of direct address */
    };

    需要注意的是,socket并不只是为TCP服务的,它支持多种协议,而各种协议的地址格式又大相径庭。因此,我们在socket相关的API中不可能用同样的地址结构来描述地址信息,这里的sockaddr只是一个占位符的角色,我们在实际编程中必须替换乘合适的地址类型。

    对于TCP/IP族,我们需要用的结构类型是sockaddr_in,该结构定义如下:

    struct sockaddr_in {
    short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8];
    };
    其中in_addr定义为:
    struct in_addr {
                                            union {
                                            struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                                            struct { u_short s_w1,s_w2; } S_un_w;
                                            u_long S_addr;
                                            } S_un;
                                            #define s_addr  S_un.S_addr
                                            /* can be used for most tcp & ip code */
                                            #define s_host  S_un.S_un_b.s_b2
                                            /* host on imp */
                                            #define s_net   S_un.S_un_b.s_b1
                                            /* network */
                                            #define s_imp   S_un.S_un_w.s_w2
                                            /* imp */
                                            #define s_impno S_un.S_un_b.s_b4
                                            /* imp # */
                                            #define s_lh    S_un.S_un_b.s_b3
                                            /* logical host */
                                            };
                                            

    需要注意的是,这个sockaddr_in结构和sockaddr结构尺寸一样,我不清楚这是巧合还是必须遵守的一个准则。在我看来,这个结构不一样也是可以的。

    我不打算一一介绍各个宏和域的含义,因为绝大多数人都不会记住这个。我只想简要介绍一下如何初始化这个地址信息

    1. sa_family:这个域描述了地址族信息。对于TCP/IP,这个值必须设置为AF_INET。有兴趣的朋友可以到WinSock.H中找找看它还支持哪些值。
    2. sin_port:端口号,对于我们提供的端口号,必须用htons转换一下再赋值,方法是:
      addr.sin_port = htons(port);
    3. sin_addr:地址,这里应该是IP地址。我们可以用inet_addr函数从点分式IP地址转换得到这个IP地址,方法是:
      addr.sin_addr.s_addr = inet_addr("100.101.102.103");
      注意:这里使用了宏s_addr,该宏的定义请参考上文。
    4. sin_zero:填充信息,必须设置为0。初学编程的人往往忘记初始化这个域,根据我的经验,这会导致函数调用失败。

    由于API中都使用sockaddr结构,因此在使用时必须进行强制类型转换,并提供结构尺寸信息。下面就是accept函数调用时的例子:

    sockaddr_in addr;
                                            memset(&addr, 0, sizeof(addr));
                                            addr.sa_family = AF_INET;
                                            int size = sizeof(addr);
                                            SOCKET sd = accept(serversd, (sockaddr*)&addr, &size);
                                            
    发表于 2005-10-06 11:47 馨荣家园 阅读(645) | 评论 (6)编辑 收藏

    2005年10月2日 #

    说明 下一篇
    由于我对TCP稍微熟悉,所以本系列文章只描述socket(套接字)接口的TCP编程

    在IT领域中,大家经常听说Client/Server的概念,这个概念是描述了提供服务的一方(服务器)和接受服务的一方(客户端)之间的拓扑结构。举例来说,http://www.vckbase.com这个网站提供大家交流和娱乐的服务,他们的计算机对于我们而言就是服务器,而我们的计算机则是客户端。一般而言,服务器功能比较强大,一个服务器能提供服务给多个客户端。

    对于socket设计而言,我们往往也提到一个服务器端和客户端的概念,但是它的概念和前面而言基本是不一样的。这里的概念其实是指在开始通讯时谁先发起连接的过程。

    对于通讯的双方而言,存在一个建立对话通道的过程,该过程的建立必然是一方主动发起的。我们称呼主动建立链路的一方为客户端,而另外一方为服务器端。(至少我个人这么称呼,不知道业界是不是都这么说)

    在通讯过程中,服务器端必须在某个预先约定的端口等待其他主机建立链路,这个过程称为侦听(Listen)。客户端主动建立链路的过程成为连接(Connect)。当服务器端套接字接到一个连接请求后,会生成一个新的套接字。服务器端主机就通过这个套接字和客户端主机通讯。

    在整个过程中,涉及到三个套接字,他们分别是服务器端套接字(我一般称呼它为server socket)、客户端套接字(一般我称呼它为client socket)和服务器端套接字创建出来的套接字。在编程过程中,第三种套接字除了不会主动建立链路外,功能和client socket完全一致,因此我也把它称为client socket。为了区别,对于客户端的client socket,大家可以称呼它为主动client socket(Active Client Socket, ACS),而对服务器端的那个客户端套接字,则称为被动client socket(Passive Client Socket, PCS)。一般情况下,我们没有必要区分ACS和PCS,因为PCS的功能是ACS的一个部分,ACS只多一项功能而已。在后文中,我将不区分这两种client socket

    对于server socket, 它有如下功能:

    1. 等待客户端建立连接(listen)
    2. 当客户端建立发送连接请求时,能接受请求,完成链路建立(accept)
    3. 关闭(close)

    对于client socket,它有如下功能:

    1. 建立链路(connect)
    2. 发送数据(send)
    3. 接收数据(recv)
    4. 关闭(close)

    一个简单的通讯过程应该这样:

    • server socket开始侦听(listen)
    • client socket开始连接(connect)
    • server socket接受连接(accept)
    • ACS和PCS开始互相发送数据(send/recv)
    • ACS或PCS关闭链路(close)
    • server socket关闭(close)

    本系列将根据以上过程介绍socket编程的方方面面。由于阻塞套接字相对简单,我会先从阻塞套接字(也就是伯可力套接字)编程开始,之间会穿插一些多线程的知识。很多方面我也没用过,所以有些东西只能边试边写,和大家探讨。

    发表于 2005-10-02 23:06 馨荣家园 阅读(1202) | 评论 (10)编辑 收藏

    2005年8月14日 #

    It seems that I have become a Super Girls' fan.

    I never heard Li Yuchun's singing before. After packing all the belongings, I have a chance to review the six to five final. I love Bichang before. However, I am beginning to love Yuchun this time.

    Stage effect can affect the audience greatly. I love vivid actors/actresses. Li Yuchun always has some body language on the stage, which absorbs me. Well, I have to abandon Bichang now. Am I an lapsible fan?

    Actually, I am not a fan.

    posted on 2006-03-17 17:30  songyujin  阅读(2105)  评论(0编辑  收藏  举报