Windows Socket 网络编程

Windows Socket 网络编程
Windows Socket 网络编程

Windows Socket 网络编程(二) ——套接字编程原理

作者: 冰点工作室小鹰

一、客户机/服务器模式

在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model)。该模式的建立基于以下两点:1、非对等作用;2、通信完全是异步的。客户机/服务器模式在操作过程中采取的是主动请示方式:

首先服务器方要先启动,并根据请示提供相应服务:(过程如下)

1、打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求。

2、等待客户请求到达该端口。

3、接收到重复服务请求,处理该请求并发送应答信号。

4、返回第二步,等待另一客户请求

5、关闭服务器。

客户方:

1、打开一通信通道,并连接到服务器所在主机的特定端口。

2、向服务器发送服务请求报文,等待并接收应答;继续提出请求……

3、请求结束后关闭通信通道并终止。

二、基本套接字

为了更好说明套接字编程原理,给出几个基本的套接字,在以后的篇幅中会给出更详细的使用说明。

1、创建套接字——socket()

功能:使用前创建一个新的套接字

格式:SOCKET PASCAL FAR socket(int af,int type,int procotol);

参数:af: 通信发生的区域

type: 要建立的套接字类型

procotol: 使用的特定协议

2、指定本地地址——bind()

功能:将套接字地址与所创建的套接字号联系起来。

格式:int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int namelen);

参数:s: 是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。其它:没有错误,bind()返回0,否则SOCKET_ERROR

地址结构说明:

struct sockaddr_in

{

short sin_family;//AF_INET

u_short sin_port;//16位端口号,网络字节顺序

struct in_addr sin_addr;//32位IP地址,网络字节顺序

char sin_zero[8];//保留

}

3、建立套接字连接——connect()和accept()

功能:共同完成连接工作

格式:int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int namelen);

SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR * name,int FAR * addrlen);

参数:同上

4、监听连接——listen()

功能:用于面向连接服务器,表明它愿意接收连接。

格式:int PASCAL FAR listen(SOCKET s, int backlog);

5、数据传输——send()与recv()

功能:数据的发送与接收

格式:int PASCAL FAR send(SOCKET s,const char FAR * buf,int len,int flags); int PASCAL FAR recv(SOCKET s,const char FAR * buf,int len,int flags);

参数:buf:指向存有传输数据的缓冲区的指针。

6、多路复用——select()

功能:用来检测一个或多个套接字状态。

格式:int PASCAL FAR select(int nfds, fd_set FAR * readfds,fd_set FAR * writefds,

fd_set FAR * exceptfds,const struct timeval FAR * timeout);

参数:readfds:指向要做读检测的指针

writefds:指向要做写检测的指针

exceptfds:指向要检测是否出错的指针

timeout:最大等待时间

select()* 执行同步I/O多路复用。

select函数的参数( int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, const struct timeval timeout )

我记得是:第一个是个较为次要的值,设成0就行了。后面的几个FD_SET类

型的参数才是最重要的;

第一个FD_SET型的参数readfds是表示要被检查是否可读的Sockets,把你想

要接收数据的那个套接字放在这里;

第二个FD_SET参数ritefds是表示要被检查是否可写的Sockets,将你要发送

数据的套接字放在这里;

还有个FD_SET参数exceptfds是表示要被检查是否有错误的Sockets select()

函数的第五个参数timeout,是让我们用来设定select 函数要等待(block)多久。

兹述说如下:

(1)如果timeout 设为「NULL」,那么select() 就会一直等到「至少」某一

个socket 的事件成立了才会return,这和其他的blocking 函数一样。

select( ..., NULL )

(2)如果timeout 的值设为{0, 0} (秒, 微秒),那么select() 在检查后,

不管有没有socket 的事件成立,都会马上return,而不会停留。

https://www.360docs.net/doc/d41934530.html,_sec = https://www.360docs.net/doc/d41934530.html,_usec = 0; select( ..., &timeout )

(3)如果timout 设为{m, n},那么就会等到至少某一个socket 的事件发生,或是时间到了(m 秒n 微秒),才会return。https://www.360docs.net/doc/d41934530.html,_sec = m; https://www.360docs.net/doc/d41934530.html,_usec = n; select( ..., &timeout )

返回值:成功- 符合条件的Sockets 总数(若Timeout 发生,则为0) 失败- SOCKET_ERROR (呼叫WSAGetLastError() 可得知原因)

说明:使用者可利用此函式来检查Sockets 是否有资料可被读取,或是有空间

可以写入,或是有错误发生。

关于对FD_SET类型的操作,有几个比较重要的宏:FD_ZERO(*set) -- 将set 的值清乾净FD_SET(s, *set) -- 将s 加到set 中FD_CLR(s, *set) -- 将s

从set 中删除FD_ISSET(s, *set) -- 检查s 是否存在於set 中参数readfds、writefds、及exceptfds 都是「called by value- result」;而「called

by value-result」的意思就是说,我们在将参数传给系统时,要先设启始值,并将这些参数的位址(address)告诉系统;而系统则会利用到这些值来做些运算或其他用途,最后并将结果再写回这些参数的位址中。因此这些参数的值在传入前和函数返回后,可能会不同;所以每次调用select() 前,对这些参数一定要重新设定它们的值。假设我们要检查socket 1 和 2 目前是否可以用来传送资料,以及socket 3 是否有资料可读;我们不打算检查sockets 是否有错误发生,所以exceptfds 设为NULL。步骤大致如下:FD_ZERO( &writefds ); FD_ZERO( &readfds ); FD_SET( 1, &writefds ); FD_SET( 2, &writefds );

FD_SET( 3, &readfds ); select( ..., &readfds, &writefds, NULL, ...) if

(FD_ISSET( 1, &writefds )) send( 1, data ); if (FD_ISSET( 2, &writefds )) send( 2, data ); if (FD_ISSET( 3, &readfds )) recv( 3, data );

7、关闭套接字——closesocket()

功能:关闭套接字s

格式:BOOL PASCAL FAR closesocket(SOCKET s);

三、典型过程图

2.1 面向连接的套接字的系统调用时序图

2.2 无连接协议的套接字调用时序图

2.3 面向连接的应用程序流程图

FD_ZERO,FD_ISSET这些都是套节字结合操作宏

看看MSDN上的select函数,

这是在select io 模型中的核心,用来管理套节字IO的,避免出现无辜锁定.

int select( int nfds,fd_set FAR *readfds, fd_set FAR *writefds,

fd_set FAR *exceptfds,

const struct timeval FAR *timeout

);

第一个参数不管,是兼容目的,最后的是超时标准,select是阻塞操作

当然要设置超时事件.

接着的三个类型为fd_set的参数分别是用于检查套节字的可读性,可写性,和列外数据性质.

我举个例子

比如recv(), 在没有数据到来调用它的时候,你的线程将被阻塞

如果数据一直不来,你的线程就要阻塞很久.这样显然不好.

所以采用select来查看套节字是否可读(也就是是否有数据读了)

步骤如下

socket s;

.....

fd_set set;

while(1)

{

FD_ZERO(&set);//将你的套节字集合清空

FD_SET(s, &set);//加入你感兴趣的套节字到集合,这里是一个读数据的套节字s

select(0,&set,NULL,NULL,NULL);//检查套节字是否可读,

//很多情况下就是是否有数据(注意,只是说很多情况)

//这里select是否出错没有写

if(FD_ISSET(s, &set) //检查s是否在这个集合里面,

{ //select将更新这个集合,把其中不可读的套节字去掉

//只保留符合条件的套节字在这个集合里面

recv(s,...);

}

//do something here

}

不知道你现在明白没有.另,由于这段时间没忙这,有错误不负责任.呵呵.

1、Socket服务器端:

Socket服务器端流程如下:加载套接字->创建监听的套接字->绑定套接字->监听套接字->处理客户端相关请求。

下面是孙鑫VC详解里面的服务器端的例子:

C++代码

#include

#include

void main()

{

//加载套接字

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested=MAKEWORD(1,1);

err=WSAStartup(wVersionRequested,&wsaData);

if (err!=0)

{

return;

}

if (LOBYTE(wsaData.wVersion)!=1||

HIBYTE(wsaData.wVersion)!=1)

{

WSACleanup();

return;

}

//创建监听的套接字

SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);

SOCKADDR_IN addrSrv;

addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//把U_LONG的主机字节顺序转换为TCP/IP网络字节顺序

addrSrv.sin_family=AF_INET;

addrSrv.sin_port=htons(6000);

//绑定套接字

bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

//将套接字设置为监听模式,准备接受用户请求

listen(sockSrv,5);

SOCKADDR_IN addrClient;

int len=sizeof(SOCKADDR);

printf("%s\n","welcome,the serve is started...");

while (1)

{

//等待用户请求到来

SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);

char sendBuf[100];

sprintf(sendBuf,"welcome %s to https://www.360docs.net/doc/d41934530.html,",inet_ntoa(addrClient.sin_addr));

//发送数据

send(sockConn,sendBuf,100,0);

char revBuf[100];

//接收数据

recv(sockConn,revBuf,100,0);

//打印接受数据

printf("%s\n",revBuf);

//关闭套接字

closesocket(sockConn);

}

}

#include

#include

void main()

{

//加载套接字

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested=MAKEWORD(1,1);

err=WSAStartup(wVersionRequested,&wsaData);

if (err!=0)

{

return;

}

if (LOBYTE(wsaData.wVersion)!=1||

HIBYTE(wsaData.wVersion)!=1)

{

WSACleanup();

return;

}

//创建监听的套接字

SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0);

SOCKADDR_IN addrSrv;

addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//把U_LONG的主机字节顺序转换为TCP/IP网络字节顺序

addrSrv.sin_family=AF_INET;

addrSrv.sin_port=htons(6000);

//绑定套接字

bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

//将套接字设置为监听模式,准备接受用户请求

listen(sockSrv,5);

SOCKADDR_IN addrClient;

int len=sizeof(SOCKADDR);

printf("%s\n","welcome,the serve is started...");

while (1)

{

//等待用户请求到来

SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len);

char sendBuf[100];

sprintf(sendBuf,"welcome %s to https://www.360docs.net/doc/d41934530.html,",inet_ntoa(addrClient.sin_addr));

//发送数据

send(sockConn,sendBuf,100,0);

char revBuf[100];

//接收数据

recv(sockConn,revBuf,100,0);

//打印接受数据

printf("%s\n",revBuf);

//关闭套接字

closesocket(sockConn);

}

}

注意:需要包含头文件,并且在工程设置的link里面加上ws32_2.dll

如果在VC中还有一个简单的加载套接字的方法:

C++代码

if (!AfxSocketInit())

{

AfxMessageBox("套接字加载失败!");

return false;

}

if (!AfxSocketInit())

{

AfxMessageBox("套接字加载失败!");

return false;

}

这个不需要包含上面注里面的头文件和ws2_32.lib库就可以实现加载套接字。

2、Socket客户端:

Socket客户端同样需要先加载套接字,然后创建套接字,不过之后不用绑定和监听了,而是直接连接服务器,发送相关请求。

同样贴出孙鑫VC详解里面的客户端的例子:(不是我偷懒,是人家实在写的太好,无法超越)

C++代码

#include

#include

void main()

{

//加载套接字

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested=MAKEWORD(1,1);

err=WSAStartup(wVersionRequested,&wsaData);

if (err!=0)

{

return;

}

if (LOBYTE(wsaData.wVersion)!=1||

HIBYTE(wsaData.wVersion)!=1)

{

WSACleanup();

return;

}

//创建套接字

SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);

SOCKADDR_IN addrSrv;

addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//把U_LONG的主机字节顺序转换为TCP/IP网络字节顺序

addrSrv.sin_family=AF_INET;

addrSrv.sin_port=htons(6000);

//向服务器发送请求

connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

//接受数据

char recBuf[100];

recv(sockClient,recBuf,100,0);

printf("%s\n",recBuf);

//发送数据

send(sockClient,"this is 扈修非",strlen("this is 扈修非")+1,0);

//关闭套接字

closesocket(sockClient);

WSACleanup();

}

#include

#include

void main()

{

//加载套接字

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested=MAKEWORD(1,1);

err=WSAStartup(wVersionRequested,&wsaData);

if (err!=0)

{

return;

}

if (LOBYTE(wsaData.wVersion)!=1||

HIBYTE(wsaData.wVersion)!=1)

{

WSACleanup();

return;

}

//创建套接字

SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);

SOCKADDR_IN addrSrv;

addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//把U_LONG的主机字节顺序转换为TCP/IP网络字节顺序

addrSrv.sin_family=AF_INET;

addrSrv.sin_port=htons(6000);

//向服务器发送请求

connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));

//接受数据

char recBuf[100];

recv(sockClient,recBuf,100,0);

printf("%s\n",recBuf);

//发送数据

send(sockClient,"this is 扈修非",strlen("this is 扈修非")+1,0);

//关闭套接字

closesocket(sockClient);

WSACleanup();

}

需要加载的头文件和库同上

相关主题
相关文档
最新文档