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上提问的人一样,大概这时候只能喊“为什么连接失败啊?”。这种情况下,其实只能猜测失败的原因,即使高手,也无法准确说出失败的原因。
增加诊断信息
在知道错误的情况下,应该尽可能的告诉测试、使用者更多的信息,这样才能了解导致失败的原因。如果程序员能提供如下错误信息,对于诊断错误是非常有帮助的:
- 出错的文件:我们可以借助宏THIS_FILE和__FILE__。注意THIS_FILE是在cpp文件手工定义的,而__FILE__是编译器定义的。当记录错误的函数定义在.h中时,有时候用THIS_FILE更好,因为他能说明在哪个cpp中调用并导致失败的。
- 出错的行:我们可以借助宏__LINE__
- 出错的函数:如果设计的好,有以上两项已经足够。当然我们可以直接打印出出错的函数或者表达式,这样在大堆代码中搜索(尤其是不支持go to line的编辑器中)还是很有用的。大家可以参见我的文章http://blog.vckbase.com/arong/archive/2005/11/10/14704.html中的方式进行处理,也许是一个基本的开端。
- 出错的原因:出错的原因很多只能由程序自己给出。如果出错只会问别人,那么你永远不可能成为一个合格的程序设计人员。很多函数失败时都会设置errno。我们可以用GetLastError获得错误码,并通过FormatMessage打印出具体错误的文字描述。
终了
给初学者一个忠告:编程时麻烦10分钟,调试时省却数小时,要想省时间,还是要从代码的可重用性和可维护性上下功夫,而不是两个代码上节省。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的时候这么做,因为这增加了出错的风险(尤其当出现拆解包问题时,有人称为粘包,我基本不认同这种称呼)。如果一定要这么做,方法应该是:
- 当前拥有这个socket的线程调用Detach方法,这样socket句柄和C++对象及当前线程脱离关系
- 当前线程把这个对象传递给另外一个线程
- 另外一个线程创建新的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年11月20日 #
x = x+1
和x++
前者需要两个mov指令和一个add指令,而后者只需要一个inc指令,在效率至上年代,这是很重要的。
但是,现在的软件都很大,一两个指令的优化对程序基本上没有任何意义。而程序的可维护性和可读性则在大软件中更为重要。由于++运算符和--运算符在复杂表达式中往往受运算顺序影响,其结果是无法预知的,因此现代软件工程往往对这两个指令持反对态度。
考虑常见的国内垃圾考试题,计算a= (b++) + (b--) 的值,这种题目到底结果如何,老实说,即使知道也没有任何意义,因为你根本不应该写出这样的式子。合理的式子应该是:
a = (b+1) + (b-1);
b++;
建议尤其是新学生,把不用++或者--当作一个教条,这样对你只有好处,没有害处。如果一定要用,必须保证一个表达式就是一个变量和++或者--的结合,也就是只能使用如下四种形式:
a++;
a--;
++a;
--a;
2005年11月10日 #
- 初始化
- 创建套接字
- 绑定本地地址
- 进入侦听状态
- 处理接受循环
下面首先创建一个例子来演示服务端套接字的实现,并在以后的各节中优化这个设计。
这个设计实现的功能如下:允许客户端(实际上就是telnet程序)登陆,并对客户端的输入回显。
4.1 准备工程
为了实现这个demo,我打算使用Visual Studio 6.0提供的对话框模板来实现,请按照下述步骤准备工程:
- 启动Visual C++ 6.0
- 选择File/New/Project
- 选择MFC AppWizard(exe)
- 在Project name中输入demo1, Next
- 选择Dialog Base, Next
- 在Windows Sockets前打钩
- 其他都保持缺省值,点击Next完成向导
- 在工程中加入两个文件:sockutil.h和sockutil.cpp:这两个文件将保存公共的函数,避免以后重复编写。
这样创建的工程会自动加入WinSock2支持,具体的内容包括:
- 在stdafx.h中加入#include <afxsock.h>,该头文件包含如下语句载入对应LIB:
#pragma comment(lib, "wsock32.lib") - 在InitInstance中加入如下代码:
if(!AfxSocketInit()) { AfxMessageBox(IDP_SOCKETS_INIT_FAILED); return FALSE; }
- 在资源中定义字符串资源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月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年10月31日 #
服务端套接字是编程最简单的一个部分,甚至在各种环境下都可以用类似的代码。对于一个服务端套接字而言,他的基本工作包括:
- 初始化
- 创建套接字
- 给套接字捆绑一个本地地址
- 【可选】设置套接字属性
- 调用listen函数
- 进入accept循环,接受来自客户端的请求
- 对PCS进行管理
- 释放服务端套接字所占用的资源
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月6日 #
2。本人提供的代码都是简单测试即写出,因此不保证在您的系统中能够运行,也不提供后续的调试支持。
3。由于我个人也比较忙,因此可能提供文章的过程不够连续,所以希望大家耐心。
如果其中有任何错误,请把错误信息发送给arongustc@hotmail.com。在本站留言可能会被我忽略。但是请不要发邮件去讨论您编程中的问题,因为实在没有时间回复各位。
谢谢。
所有的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 {其中in_addr定义为:short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8];};
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结构尺寸一样,我不清楚这是巧合还是必须遵守的一个准则。在我看来,这个结构不一样也是可以的。
我不打算一一介绍各个宏和域的含义,因为绝大多数人都不会记住这个。我只想简要介绍一下如何初始化这个地址信息
- sa_family:这个域描述了地址族信息。对于TCP/IP,这个值必须设置为AF_INET。有兴趣的朋友可以到WinSock.H中找找看它还支持哪些值。
- sin_port:端口号,对于我们提供的端口号,必须用htons转换一下再赋值,方法是:
addr.sin_port = htons(port); - sin_addr:地址,这里应该是IP地址。我们可以用inet_addr函数从点分式IP地址转换得到这个IP地址,方法是:
addr.sin_addr.s_addr = inet_addr("100.101.102.103");
注意:这里使用了宏s_addr,该宏的定义请参考上文。 - 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月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, 它有如下功能:
- 等待客户端建立连接(listen)
- 当客户端建立发送连接请求时,能接受请求,完成链路建立(accept)
- 关闭(close)
对于client socket,它有如下功能:
- 建立链路(connect)
- 发送数据(send)
- 接收数据(recv)
- 关闭(close)
一个简单的通讯过程应该这样:
- server socket开始侦听(listen)
- client socket开始连接(connect)
- server socket接受连接(accept)
- ACS和PCS开始互相发送数据(send/recv)
- ACS或PCS关闭链路(close)
- server socket关闭(close)
本系列将根据以上过程介绍socket编程的方方面面。由于阻塞套接字相对简单,我会先从阻塞套接字(也就是伯可力套接字)编程开始,之间会穿插一些多线程的知识。很多方面我也没用过,所以有些东西只能边试边写,和大家探讨。
2005年8月14日 #
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) 编辑 收藏 举报