Windows网络编程套接字的类型
套接字的类型:
- 流式套接字(SOCK_STREAM):提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且发送顺序接收
- 数据报式套接字(SOCK_DGRAM):提供无连接服务。数据包以独立包形式发送,不提供无措保证,数据可能丢失或重复,并且接收顺序混乱
- 原始套接字(SOCK_RAW):WinSock接口并不使用某种特定的协议去封装它,而是有程序自行处理数据报以及协议首部
基于TCP(面向连接)的 socket 编程
一、服务端程序
- 创建套接字(socket)。
- 将套接字绑定到一个本地地址和端口上(bind)。
- 将套接字设为监听模式,准备接受客户端请求(client)。
- 等待客户端请求到来,当请求到来后,接收连接请求,返回一个新的对应于此次连接的套接字(accept)。
- 用返回的套接字和客户端进行通信(send/recv)。
- 返回,等待另一个客户请求。
- 关闭套接字。
二、客户端程序:
- 创建套接字(socket)。
- 向服务器发送连接请求(connect)。
- 和服务器端进行通信(send/recv)。
- 关闭套接字。
三、一个简单的通信案例
① 打开VC6.0创建一个新的空的控制台项目
② 为项目添加wsock32.lib 链接库,可使用
#pragma comment(lib,"wsock32.lib")
或者 Project->Settings->Link 在Object/library modules 添加 ws2_32.lib
③ 创建一个TcpSrv.cpp用来编写服务端程序,详细代码如下:
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"wsock32.lib")
int main()
{
//WSAStartup的参数
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
//将地位和高位连接起来创建一个WORD类型的值
/*WSAstartup必须是应用程序首先调用的WInsock函数,
它允许应用程序指定所需的Windows Sockets API 的版本,获取特定WInsock实现的详细信息。
仅当这个函数成功执行之后,应用程序才能调用其他WInsock API*/
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
//执行成功后err应该是0,否则出错要调用 WSAGetLastError 函数查看出错的原因。
return 0;
}
//如果获取的版本号的地位高位不是所设置的,那么使用WSACleanup进行释放
//每个WSAStartup的调用必须对应一个对WSACleanup的调用,这个函数释放WInsock库
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return 0;
}
//创建套接字对象
SOCKET sockSrv = socket(AF_INET,SOCK_STREAM,0);
//设置将要绑定的地址和端口号
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
//绑定套接字到指定的IP地址和端口号
bind(sockSrv,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR));
//设置套接字进入监听状态
listen(sockSrv,5);
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
//循环监听
while(TRUE)
{
//等待接收客户端的连接
SOCKET sockConn = accept(sockSrv,(SOCKADDR *)&addrClient,&len);
char sendBuf[100];
//格式化字符串,将要发送给客户端
sprintf(sendBuf,"Welcome %s to my server",inet_ntoa(addrClient.sin_addr));
//向客户端发送信息
send(sockConn,sendBuf,strlen(sendBuf)+1,0);
char recvBuf[100];
//接收客户端的消息
recv(sockConn,recvBuf,100,0);
printf("%s\n",recvBuf);
//关闭socket套接字
closesocket(sockConn);
}
return 0;
}
④ 创建一个新的空的控制台工程,添加lib后,创建一个TcpClient.cpp用于编写客户端连接程序,代码如下:
#include <winsock2.h>
#include <stdio.h>
int main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return 0;
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return 0;
}
//以上代码均为WInsock编程必要的步骤
//创建客户端socket
SOCKET sockClient = socket(AF_INET,SOCK_STREAM,0);
//设置socket连接的地址和端口
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port=htons(6000);
//进行连接
connect(sockClient,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR));
char recvBuf[100];
//接收消息
recv(sockClient,recvBuf,100,0);
printf("%s\n",recvBuf);
char sendBuf[100] = "你好,网络世界!";
//发送消息
send(sockClient,sendBuf,sizeof(sendBuf)+1,0);
//关闭socket
closesocket(sockClient);
//清除资源
WSACleanup();
return 0;
}
运行结果如下:
Server:
Client:
基于UDP(面向无连接)的 socket 编程
一、服务器端(接收端)程序:
① 创建套接字(socket)
② 将套接字绑定到一个本地地址和端口上(bind)
③ 等待接收数据(recvfrom)
④ 关闭套接字
二、客户端(发送端)程序:
① 创建套接字(socket)
② 向服务器发送数据(sendto)
③ 关闭套接字
三、UDP 编程的简单例子
创建工程步骤略
服务器端代码:
#include <winsock2.h>
#include <stdio.h>
int main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return 0;
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return 0;
}
//以上代码为Winsocket编程必要步骤,说明略
//创建服务端socket,并设置端口
SOCKET sockSrv = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
//绑定socket信息
bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
char recvBuf[100];
//接收客户端信息
recvfrom(sockSrv,recvBuf,strlen(recvBuf),0,(SOCKADDR*)&addrClient,&len);
printf("%s\n",recvBuf);
//关闭socket
closesocket(sockSrv);
//清除资源
WSACleanup();
return 0;
}
客户端程序:
#include <winsock2.h>
#include <stdio.h>
int main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD( 1, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 ) {
return 0;
}
if ( LOBYTE( wsaData.wVersion ) != 1 ||
HIBYTE( wsaData.wVersion ) != 1 ) {
WSACleanup( );
return 0;
}
//以上代码为Winsocket编程必要步骤,说明略
//创建客户端socket
SOCKET sockClient = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addrSrv;
//设置发送地址
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_family = AF_INET;
//发送端口
addrSrv.sin_port = htons(6000);
char sendBuf[100] = "你好啊,UDP世界";
//发送数据报
sendto(sockClient,sendBuf,strlen(sendBuf)+1,0,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));
//关闭socket
closesocket(sockClient);
//清除资源
WSACleanup();
return 0;
}
运行结果如下:
Server:
Client:
基础API函数说明
1.寻址方式
因为Winsock 要兼容几个协议,所以必须使用通用的寻址方式。TCP/IP使用IP地址和端口号来指定一个地址,但是其他协议也许采用不同的形式。如果Winsock 强迫使用特定的寻址方式,添加其他协议就不大可能了。Winsock的第一个版本使用sockaddr结构来解决此问题
① sockaddr
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
在这个结构中,第一个成员sa_family 指定了这个地址使用的地址家族。sa_data成员存储的数据在不同的地址家族中可能不同。Winsock已经定义了sockaddr结构的TCP/IP版本——sockaddr_in结构。它们本质上是相同的结构,但是第2个更容易操作。
②sockaddr_in
struct sockaddr_in
{
short sin_family; //地址家族
u_short sin_port; //端口号
struct in_addr sin_addr; //IP 地址
char sin_zero[8]; //空字节,要设为0
}
此结构的最后8个字节没有使用,是为了与sockaddr结构大小相同才设置的。
sin_addr是IP地址(32位),它被定义为一个联合来处理整个32位的值,两个16位部分或者每个字节单独分开。描述32位IP地址的in_addr结构定义如下:
③ in_addr
struct in_addr{
union{
struct{ u_char s_b1,s_b2,s_b3,s_b4;} S_un_b; //以4个u_char来描述
struct{ u_short s_w1,s_w2;} S_un_w; //以2个u_short来描述
u_long S_addr; //以1个u_long来描述
}S_un;
}
用字符串“aa.bb.cc.dd”表示IP地址时,字符串中由点分开的4个域是以字符串的形式对in_addr结构中的4个u_char值得描述。由于每个字节的数值范围是0~255,所以各域的值是不可以超过255的。
2.字节顺序
字节顺序是长度跨越多个字节的数据被存储的顺序。例如,一个32位的长整型0x12345678跨越4个字节(每个字位8位)。Intel x86 机器使用小尾顺序(little-endian),意思是最不重要的字节首先存储。因此,数据0x12345678在内存中的存放顺序是0x78、0x56、0x34、0x12。大多数不使用小尾顺序的机器使用大尾顺序(big-endian),即最重要的字节首先存储。同样的值在内存中的存放顺序将是0x12、0x34、0x56、0x78。因为协议数据要在这些机器间传输,就必须选定其中的一种方式作为标准,否则会引起混淆。
TCP/IP协议统一规定使用大尾方式传输数据,也称为网络字节顺序。例如,端口号(它是一个16位的数字)12345(0x3039)的存储顺序是0x30、0x39。32位的IP地址也是以这种方式存储的,IP地址的每一部分存储在一个字节中,第一部分存储在第一个字节中。
上述sockaddr 和 sockaddr_in结构中,除了sin_family成员(它不是协议的一部分)外,其他所有值必须以网络字节顺序存储。Winsock提供了一些函数来处理本地机器的字节顺序如网络字节顺序的转换。
- u_short htons(u_short hostshort); //转化一个u_short类型从主机字节顺序到TCP/IP网络字节顺序
- u_long htonl(u_long hostlong); //转化一个u_long类型从主机字节顺序到TCP/IP网络字节顺序
- u_short ntohs(u_short netshort); //转化一个u_short类型从TCP/IP网络字节顺序到主机字节顺序
- u_long ntohl(u_long netlong); //转化一个u_long 类型从TCP/IP网络字节顺序到主机字节顺序
这些API是平台无关的,使用他们可以保证程序正确地运行在所有机器上。
3. 使用举例
在sockaddr_in结构中除了sin_family成员之外,所有的成员必须以网络字节顺序存储。下面是初始化sockaddr_in结构的例子
sockaddr_in sockAddr1,sockAddr2;
//设置地址家族
sockAddr1.sin_family = AF_INET;
//转化端口号80到网络字节顺序,并安排它到正确的成员
sockAddr1.sin_port = htons(80);
//inet_addr 函数转化一个“aa.bb.cc.dd”类型的IP地址字符串到长整型,
//它是以网络字节顺序记录的IP地址
//sin_addr.S_un.S_addr 指定了地址联合中的此长整型
sockAddr1.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
//通过设置4个字节部分,设置sockAddr2的地址
sockAddr2.sin_addr.S_un.S_un_b.s_b1 = 127;
sockAddr2.sin_addr.S_un.S_un_b.s_b2 = 0;
sockAddr2.sin_addr.S_un.S_un_b.s_b3 = 0;
sockAddr2.sin_addr.S_un.S_un_b.s_b4 = 1;
上例中inet_addr函数将一个由小数点分隔的十进制IP地址字符串转化成由32位二进制数表示的IP地址(网络自己顺序)。inet_ntoa是inet_addr函数的逆函数,它将一个网络字节顺序的32位IP地址转化成字符串。
char *inet_ntoa(struct in_addr in); //将32位的二进制数转化为字符串
4、Winsock库函数
所有的Winsock函数都是从WS2_32.dll库导入的,VC++在默认情况下并没有连接到该库,如果想使用Winsock API,就必须包含相应的库文件
#pragma comment(lib,"wsock32.lib")
① Winsock库的装入、初始化和释放
int WSAStartup(
WORD wVersionRequested, //应用程序支持的最高WinSock库版本。高字节为次版本号,低字节为主版本号
LPWSADATA lpWSAData //一个指向WSADATA结构的指针。它用来返回DLL库的详细信息
);
lpWSAData 参数用来取得DLL库的详细信息,结构定义如下:
typedef struct WSAData{
WORD wVersion; //库文件建议应用程序使用的版本
WORD wHighVersion; //库文件支持的最高版本
char szDescription[WSADESCRIPTION_LEN+1]; //库描述字符串
char szSystemStatus[WSASYS_STATUS_LEN+1]; //系统状态字符串
unsigned short iMaxSockets; //同时支持的最大套接字的数量
unsigned short iMaxUdpDg; //2.0版中已废弃的参数
char FAR * lpVendorInfo; //2.0版中已废弃的参数
}WSADATA,FAR* LPWSADATA;
函数调用成功后返回0。否则要调用WSAGetLastError函数查看出错的原因。
int WSACleanup(void); //对应于一个WSAStartup(),释放Winsock库
② 套接字的创建和关闭
SOCKET socket(
int af, //用来指定套接字使用的地址格式,Winsock中支持AF_INET
int type, //用来指定套接字的类型
int protocol //配合type参数使用,用来指定使用的协议类型,可以是IPPROTO_TCP等
);
type参数用来指定套接字的类型。套接字有流式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)和原始套接字(SOCK_RAW)。
当type参数指定为SOCK_STREAM和SOCK_DGRAM时,系统已经明确确定使用TCP和UDP协议来工作,所以protocol可以设置为0.
int closesocket(SOCKET s); //函数唯一的参数就是要关闭的套接字句柄
③ 绑定套接字到指定的IP地址和端口号
int bind(
SOCKET s, //套接字句柄
const struct sockaddr *name, //要关联的本地地址
int namelen //地址的长度
bind函数用在没有建立连接的套接字上,它的作用是绑定面向连接的或者无连接的套接字。当一个套接字被socket函数创建以后,他存在于指定的地址家族里,但是它是未命名的。bind函数通过安排一个本地名称到未命名的socket建立此socket的本地关联。本地名称包含3个部分:主机地址、协议号(分别为UDP或TCP)和端口号。
示例:
//填充sockaddr_in 结构
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);
sin.sin_addr.S_un.S_addr = INADDR_ANY;
//绑定这个套接字到一个本地地址
if(::bind(s,(LPSOCKADDR)&sin,sizeof(sin))==SOCKET_ERROR)
{
printf("Failed bind() \n");
::WSACleanup();
return 0;
}
sockaddr_in 结构中的sin_familly字段用来指定地址家族,该字段和socket函数中的af参数的含义相同,所以唯一可以使用的值就是AF_INET。sin_port字段和sin_addr字段分别指定套接字需要绑定的端口号和IP地址。放入这两个字段的数据的字节顺序必须是网络字节顺序。由于网络字节顺序和Intel CPU的字节顺序刚好相反,所以必须首先用htons
函数进行转换。
如果应用程序不关系所使用的地址,可以为互联网地址指定INADDR_ANY,为端口号指定0。如果互联网地址等于INADDR_ANY,系统会自动使用当前主机配置的所有IP地址,这简化了程序设计。
④ 设置套接字进入监听状态
int listen(
SOCKET s, //套接字句柄
int backlog //监听队列中允许保持的尚未处理的最大连接数量
⑤ 接收连接请求
SOCKET accept(
SOCKET s, //套接字句柄
struct sockaddr* addr, //一个指向sockaddr_in结构的指针,用于取得对方的地址信息
int * addrlen //是一个指向地址长度的指针
该函数在s上取出未处理链接中的第一个连接,然后为这个连接创建一个新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与s有相同的属性。
程序默认工作在阻塞模式下,这种方式下如果没有未处理的连接存在,accept函数会一直等待下去直到有新的连接发生才返回。
addrlen参数用于指定addr所指空间的大小,也用于返回地址的实际长度。如果addr活着addrlen是NULL,则没有关于远程地址的信息返回
⑥ 客户端连接
int connect(
SOCKET s, //套接字句柄
const struct sockaddr FAR * name, //一个指向sockaddr_in结构的指针,包含了要连接的服务器的地址信息
int namelen //sockaddr_in 结构的长度
第一个参数s是此连接使用的客户端套接字。另两个参数name和namelen用来寻址远程套接字(正在监听的服务器套接字)。
⑦ 收发数据
int send(
SOCKET s, //套接字句柄
const char FAR* buf, //要发送数据的缓冲区地址
int len, //缓冲区长度
int flags //指定了调用方式,通常设位0
);
int recv(SOCKET s,char FAR* buf,int len,int flags);
send函数在一个连接的套接字上发送缓冲区内的数据,返回发送数据的实际字节数。recv函数从对方接受数据,并存储它到指定的缓冲区。flags参数在这两函数中通常设为0。
在阻塞模式下,send将会阻塞线程的执行直到所有的数据发送完毕(或者一个错误发生),而recv函数将返回尽可能多的当前可用信息,一直到缓冲区指定的大小。