您的位置:首页 > 理论基础 > 计算机网络

详细讲解如何使用Winsock实现网络通信——服务器端

2013-05-27 11:24 555 查看
Winsock接口实际上是微软提供的一些列API函数。它都包含在Winsock2.h中。使用的时候我们还要连接函数的导入库文件:WS2_32.lib。具体使用方法如下:

#include <Winsock2.h>

#pragma comment(lib,"ws2_32.lib")

实现网络通信,一般我们需要一个服务器端和一个客户端。

整个实现通信的一般过程如下:

服务器端:

1.调用WSAStartup函数初始化WSA

2.调用socket函数建立套接字,返回套接字句柄

3.调用bind函数,关联本地地址到套接字句柄

4.调用listen函数进入监听端口状态

5.调用accept函数,等待接受客户端连接

6.客户端连接,accept函数返回有效套接字句柄

7.调用recv或send函数接受或发送数据

8.调用closesocket函数,关闭客户端套接字

9.可循环5——8,进行数据的发送和接受

10.调用closesocket函数,关闭服务器端套接字

11.调用WSACleanup函数释放winsock库

客户端:

1.调用WSAStartup函数初始化WSA

2.调用socket函数建立套接字,返回套接字句柄

3.调用connect函数,和服务器连接

4.调用recv或send函数接受或发送数据

5.可循环3——4进行收发数据

6.调用closesocket函数,关闭套接字

7.调用WSACleanup函数释放winsock库

下面来详细的讲解一下:

第一步在调用Winsock函数之前必须先装载Winsock函数库,使用WSAStartup函数。失败返回SOCKET_ERROR

int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);

第一个参数wVersionRequested是指定想要加载Winsock库的版本。第二个参数指向一个LPWSADATA结构体指针,保存返回的Winsock库的版本信息。

函数成功调用返回0.可以通过调用WSAGetLastError函数获取失败原因。

具体加载Winsock库的代码如下:

WSADATA wsaData;
WORD sockVersion=MAKEWORD(2,2);//版本是2.2
if(WSAStartup(sockVersion,&wsaData)!=0)
return 0;


介绍两个小知识点:

1.MAKEWORD函数

这个函数实际上是一个宏,作用是将两个byte类型合并成一个word类型,一个是在高8位,一个是在低8位。

2.套接字

套接字可以看做成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入套接字中,套接字将这段信息发送到另外一个套接字,应用程序通过套接字获取信息。

在完成Winsock库初始化后,我们需要调用socket函数来建立套接字

功能函数的原型:

SOCKET socket(int af,int type,int protocol);

参数af指定套接字使用的地址格式,这里只能用AF_INET。

type参数指定套接字的类型,具体可选有三种:

SOCK_STREAM:流套接字,使用TCP提供有连接的可靠传输

SOCK_DGRAM:数据报套接字,使用UDP提供的无连接不可靠传输

SOCK_RAW:原始套接字,Winsock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据报和协议首部。

Protocol是配合type使用的,用来指定协议类型。使用TCP,值是IPPROTO_TCP;如果是UDP,使用IPPROTO_UDP。

函数成功,返回一个SOCKET句柄,失败返回INVALID_SOCKET

什么是句柄?

句柄实际上就是一个长整型数据(long),它是Windows用来标识应用程序中建立或使用的对象的一个唯一标识。它就是一个标识符,通过它我们就可以引用它所标识的对象。一般Windows函数总是返回句柄来提供我们对某些对象或资源的访问。

具体创建一个套接字的代码:

SOCKET sListen=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(sListen==INVALID_SOCKET)
{
printf("socket error!\n");
return 0;
}


建立完套接字后,对于服务器端来说,我们接下来要做的是调用bind函数将本地地址和端口绑定到套接字上。对于客户端,这不是必须的,客户端在建立完套接字后可以直接调用connect函数连接服务器,它会替我们自动绑定,指定一个随机的端口。这样的话我们可以不必要调用bind函数进行地址和端口绑定。

bind函数原型:

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

s指向一个有效的套接字句柄,可以是上面返回的sListen。

addr是一个sockaddr结构体指针,指向要关联的本地地址,结构体具体如下:

struct sockaddr_in

{

short int sin_family;

unsigned short int sin_port;

struct in_addr sin_addr;

unsigned char sin_zero[8];

};

1.sin_family值必须设置成AF_INET

2.sin_port指定TCP或UDP通信服务的端口号

关于端口号的使用范围:

一般为避免与其他应用程序或系统应用程序使用的端口号冲突,一般编程使用1024-49151之间的端口号

0-1024由IANA管理,为公共服务保留

1024-49151是普通用户注册端口号

4951-65535是动态端口号

3.sin_addr存储一个32位IP地址。这个结构我们只用了解其中包含的一个联合类型S_un,联合中有一个u_long类型S_addr用来存储32为的二进制数所表示的IP地址。

使用inet_addr函数可以讲一个小数点的十进制IP转换成一个32位二进制IP

inet_ntoa,可以进行inet_addr的逆向转换,将一个32位的二进制数IP转换成小数点的十进制字符串IP表示。

最后一个参数namelen指定了sockaddr结构体的大小

4.sin_zero保留,还没有使用

函数成功返回0,失败返回SOCKET_ERROR。绑定本地ip的具体代码:

sockaddr_in sin;
sin.sin_family=AF_INET;
sin.sin_port=htons(4500);//htons可以将主机的无符号短整型数转换成网络字节序列。
sin.sin_addr.S_un.S_addr=INADDR_ANY;//表示所有可用地址
if(bind(sListen,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR)
{
printf("bind error!");
return 0;
}


完成绑定后就是调用listen函数,进入监听状态。函数原型:

int listen(SOCKET s,int backlong);

s是套接字句柄

backlong表示监听队列中允许保持的尚未处理的最大连接数量。通俗说就是同时允许的最大连接数。

函数成功返回0,失败返回SOCKET_ERROR。

注意:

listen函数只支持连接的套接字,如SOCK_STREAM类型的套接字,基于TCP有链接的套接字。UDP的则不能使用。

具体监听代码:

if(listen(sListen,5)==SOCKET_ERROR)
{
printf("listen error!");
return 0;
}


设置好套接字进入监听状态后,接下来要进入循环,可以使程序在结束一个连接后,继续处理其他连接。首先调用accept函数等待接收客户端连接。

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

s指向套接字句柄

addr指向sockaddr结构体,用来存放客户端地址信息。

addrlen指向sockaddr长度的指针。

如果是阻塞模式下,accept会一直等待,直到有一个连接到来后才继续往下执行。accept会返回一个连接客户端的套接字句柄。我们可以利用这个套接字句柄进行接收和发送数据。

通过调用send和recv函数

从套接字接收数据:

int recv(SOCKET s,char FAR *buf,int len,int flags);

向套接字发送数据

int send(SOCKET s,const char FAR *buf,int len,int flags);

可以看到接收和发送的函数的参数基本相同

s仍然是指向一个有效的套接字句柄

buf指向发送或接收数据的缓冲区

len指向数据缓冲区的长度

flags表示调用方式,一般为0

recv成功返回接收到的字节数,失败返回SOCKET_ERROR

接收或发送完数据后需要调用closesocket关闭accept返回的套接字句柄。这样就完成了一次和客户端的通信。循环完整代码:

// 循环接受客户的连接请求
sockaddr_in remoteAddr;
SOCKET sClient;
int nAddrLen = sizeof(remoteAddr);
char revData[255];
while(TRUE)
{
// 接受一个新连接
sClient = accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
//accept函数调用失败则继续等待连接。
if(sClient == INVALID_SOCKET)
{
printf("accept() error");
continue;
}
//打印出连接者的ip
printf(" 接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
//直到收到有效数据时才打印出来
int ret=recv(sClient,revData,255,0);
if(ret>0)
{
//为了防止打印出错,把字符串结尾设成0x00
revData[ret]=0x00;
printf(revData);
}
char *buff="\r\nzy_dreamer,server coming...\r\n";
//发送数据
send(sClient,buff,strlen(buff),0);
// 关闭套接字句柄,结束会话
closesocket(sClient);
}


最后在退出服务器端程序时,需要调用closesocket函数关闭我们自己创建的socket句柄,然后调用WSACleanup函数释放Winsock库。

完整服务器端代码如下:

#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib,"ws2_32")
#include <stdio.h>
int main(int argc, char* argv[])
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
//加载winsock库
if(WSAStartup(sockVersion, &wsaData) != 0)
return 0;
// 创建套节字
SOCKET sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sListen == INVALID_SOCKET)
{
printf("socket error\n");
return 0;
}
// 在sockaddr_in结构中装入地址信息
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4500); // htons函数 将主机的无符号短整形数转换成网络
//字节顺序
sin.sin_addr.S_un.S_addr = INADDR_ANY;
// 使套接字和本地地址绑定
if(bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
{
printf(" bind error \n");
closesocket(sListen);
return 0;
}
// 设置套接字进入监听模式
if(listen(sListen, 5) == SOCKET_ERROR)
{
printf("listen error\n");
closesocket(sListen);
return 0;
}
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
SOCKET sClient;//保存客户端套接字
int nAddrLen = sizeof(remoteAddr);
char revData[255];
while(TRUE)
{
// 接受一个新连接,阻塞模式下,会一直等待
sClient = accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
//accept函数调用失败则继续等待下一次连接。
if(sClient == INVALID_SOCKET)
{
printf("accept() error");
continue;
}
//打印出连接者的ip
printf(" 接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
//直到收到有效数据时才打印出来
int ret=recv(sClient,revData,255,0);
if(ret>0)
{
//为了防止打印出错,把字符串结尾设成0x00
revData[ret]=0x00;
printf(revData);
}
char *buff="\r\nzy_dreamer,server coming...\r\n";
//发送数据
send(sClient,buff,strlen(buff),0);
// 关闭套接字句柄,结束会话
closesocket(sClient);
}
closesocket(sListen);
WSACleanup();
return 0;
}


改进:可以将收发数据的功能放到单独的线程中执行,可以提高程序效率和友好性。

下一篇讲解客户端文章地址:/article/8343179.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐