WinSock 2.0网络套接字编程

WinSock 2.0网络套接字编程
WinSock 2.0网络套接字编程

第五章WinSock 2.0网络套接字编程

5.1 套接字基本概念

套接字是应用程序通信的基石,是支持TCP/IP协议的网络通信应用的基本操作单元。可以将套接字看作是不同主机间的进程进行双向通信的端点:网络中两台通信的主机各自在自己机器上建立通信的端点──套接字,然后使用套接字进行数据通信。

一个套接字是如下描述的一个结构:

{协议,本地地址,本地端口,远程地址,远程端口}

操作系统会为本地建立的套接字分配一个唯一的套接字标识号,应用程序按该标识号来使用套接字进行网络通信。

根据网络通信的特征,套接字主要分为两类:流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。

流套接字是面向连接的,它提供双向的、有序的、无差错、无重复并且无记录边界的数据流服务,适用于处理大量数据,提供可靠的服务。

数据报套接字是无连接的,它支持双向的数据传输,具有开销小、数据传输效率高的特点,但不保证数据传输的可靠性、有序性和无重复性,适合少量数据传输、以及时间敏感的音视频多媒体数据传输。

此外,还有一种较少使用的套接字叫原始套接字(SOCK_RAW),可以使用它对底层协议如IP或ICMP直接访问,在通信与协议开发时有时会用到。

5.2 Winsock编程原理

Winsock是Microsoft Windows平台上使用套接字的设施。它实际上是一组可供应用程序进行TCP/IP通信的API应用编程接口。Winsock分1.1版和2.x版,从Windows 98开始使用2.x版。

Winsock 2提供了一组编写网络应用程序的基本API函数,诸如创建套接字、地址绑定、侦听连接请求、发出连接请求、接受连接请求、发送和接收数据、关闭套接字,等等。这些Winsock 2所用API函数的声明、常数等等均在头文件winsock2.h内定义,用VC++6.0开发网络应用程序时,需要在主程序开头使用#include 语句,以便编译时和主程序一起参加编译。

Winsock 2所用的API函数代码的实体包含在动态链接库ws2_32.dll中。Winsock 2网络应用程序.exe运行时,在动态加载系统目录中的动态链接库ws2_32.dll后,即可调用这些动态进驻到内存的Winsock2 API函数代码实体。为了能这样做,网络应用程序.exe中需要ws2_32.dll的符号信息及相应的符号表,这些信息应在网络应用程序做链接时链接进来,从静态链接函数库ws2_32.lib中得到。所以,开发Winsock 2网络应用程序时,在VC++6.0

的“Project”→“Settings…”→“Link”选项卡中,还需在“Object/library Modules”中添加静态链接函数库ws2_32.lib,才能成功编译运行。

Windows套接字程序执行API 函数I/O操作时有阻塞和非阻塞两种模式。在阻塞模式下,执行I/O操作的Winsock函数(如accept、send、recv等)在I/O操作完成前,会一直等下去,不会立即返回程序(将控制权交还给程序);而在非阻塞模式下,调用Winsock函数进行I/O操作时,不管I/O有没有完成会立即返回。socket在初始化后默认工作在阻塞模式,可以通过ioctlsocket() 函数改变socket工作模式。

一般,网络应用程序在阻塞模式下使用Winsock 2的API库函数进行流套接字和数据报套接字编程的过程如图5.1和图5.2所示。服务器如果要支持并发客户的访问,还可以使用多线程技术进行程序设计。

5.3 Winsock API函数

在图5.1和图5.2中使用的Winsock 2的API函数简要介绍如下:

(1) Winsock DLL的初始化和结束释放

由于Winsock 2提供的API服务是以动态链接库ws2_32.dll实现的,所以必须先调用WSAStartup() 函数对ws2_32.dll进行加载初始化,协商Winsock的版本支持,并分配必要的资源。如果在调用Winsock函数前没有加载Winsock库,则会返回SOCKET_ERROR错

误,错误信息是WSANOTINITIALISED。

在应用程序关闭套接字后,还应调用WSACleanup() 函数来终止和卸载动态链接库ws2_32.dll,释放资源,以备以后使用。

我们可以用以下函数来实现Winsock 2的启动初始化,若Winsock 2初始化成功则返回true,否则返回false.

bool InitSocket()

{

WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD( 2, 0 ); //询问Winsock 2.0版本

err = WSAStartup( wVersionRequested, &wsaData );

//加载初始化Windows Sockets DLL

if ( err != 0 ) {

printf("没有Windows Socket动态库!\n");

getch();

return false;

}

if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 0 ) { printf("需要Windows Socket 2!\n");

getch();

WSACleanup( );

return false;

}

return true;

}

(2) 创建套接字

服务进程和客户进程在通信前必须创建各自的套接字,然后才能用相应的套接字进行发送、接收操作,实现数据的传输。服务进程总是先于客户进程启动,服务进程和客户进程调用socket() 函数创建套接字。Socket函数的原型如下:

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

其中,af用于指定网络地址族,一般取AF_INET,表示该套接字在Internet域中进行通信。参数type用于指定套接字类型,若取SOCK_STREAM表示要创建的套接字是面向连接时使用的流套接字,若取SOCK_DGRAM则创建无连接时的数据报套接字。参数protocol则用于当指定地址族和套接字类型有多个条目时用来限定使用特定的网络协议,一般使用时可取0,表示默认为TCP/IP协议。若套接字创建成功则该函数返回所创建的套接

字句柄SOCKET,否则产生INV ALID_SOCKET错误。实现代码举例如下:if( !InitSocket() ) return 1; //初始化Window Sockets DLL,若出错返回1

sock = socket( AF_INET, SOCK_STREAM, 0 ); //创建流式套接字

或sock = socket( AF_INET, SOCK_DGRAM, 0 ); //创建数据报套接字

(3) 地址绑定

当调用socket() 函数创建了一个套接字后,服务器必须把套接字与自己的地址(包括IP地址与端口号)显式地建立联系,以便客户端向该IP地址和端口号的服务进程请求服务,这个过程是通过调用绑定函数bind() 来实现的。

客户端一般隐式地向操作系统请求一个随机的未使用过的临时端口号,跟自己的IP地址一起,与创建的套接字建立联系,由于该临时端口号客户端程序事先是不确定的,因此不显式地使用绑定函数。bind() 函数原型如下:

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

其中,第一个参数s标识一未捆绑过的套接字句柄,它用来等待客户机的请求;第二个参数name是赋予套接字的地址,它由struct sockaddr结构表示;第三个参数namelen为name 的长度。struct sockaddr结构为:

struct sockaddr{

u_short sa_family;

char sa_data[14];

};

但是一般情况下另一个与该地址结构大小相同的sockaddr_in结构更为常用,该结构用来标识TCP/IP协议下的地址,可以方便的通过强制类型转换把sockaddr_in结构转换为sockaddr结构。socketaddr_in结构的格式如下:

struct sockaddr_in{

short sin_family;

u_short sin_port;

struct in_addr sin_addr;

char sin_zero[8];

};

其中,sin_family字段必须为AF_INET, 表示该socket为Internet地址族。sin_port字段用于指定服务器端口号。sin_addr字段是in_addr结构表示的IP地址,:

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; //我们常用该联合中的这第3个定义表示,

//该无符号长整型4字节长字是网络字节顺序表示的IP地址

} S_un;

}

字段sin_zero填充8个字节0,以使sockaddr_in结构和通用地址sockaddr结构长度一样,保持兼容。

调用bind函数一旦出错,会返回SOCKET_ERROR。

说明1:多字节数据处理时,有网络字节顺序和主机字节顺序之分:

不同的计算机处理多字节数据时的方法是不一样的,Intel x86处理器用“小头”

(little-endian)形式表示多字节,即把低字节放在前面,把高字节放在后面。而互联网传输时却正好相反,采用“大头”(big-ending)形式,即高字节在前、低字节在后。因此在网络套接字程序编写时,涉及到主机中的多字节数据,该数据内各字节排列一般用“主机字节顺序”,凡涉及到网络发送、接收的数据结构中,其多字节数据中各字节排列一般用“网络字节顺序”,这在我们编写网络套接字程序时是要时时小心的。

下面两个API函数将一个多字节数从“主机字节顺序”转换成“网络字节顺序”,函数名中的h 表示host,n 表示network,s 表示short(16位的短整型数),l 表示long(32位的长整型数):

u_long htonl(u_long hostlong); //四字节转换

u_short htons(u_short hostshort); //二字节转换

下面两个API 函数将一个多字节数从“网络字节顺序”转换成“主机字节顺序”:u_long ntohl(u_long netlong); //四字节转换

u_short ntohs(u_short netshort); //二字节转换

说明2:网络套接字程序设计时,有时要将网络字节顺序的u_long IP地址和以点分隔的十进制字符串IP地址互相转换:

inet_ntoa() 函数将网络字节顺序的u_long IP地址转换到以点分隔的十进制字符串IP地址形式,定义如下:

char FAR * WSAAPI inet_ntoa(struct in_addr in);

另一个函数inet_addr() 的作用与函数inet_ntoa() 刚好相反,它把以点分隔的十进制字符串形式表示的IP地址转换成网络字节顺序的u_long IP地址。

举例如下:

把点分隔十进制字符串形式IP地址“1.2.3.4”转换为网络字节顺序的u_long IP地址0x04030201,使用函数inet_addr()

把网络字节顺序的u_long IP地址0x04030201转换为点分隔十进制字符串形式IP地址“1.2.3.4”, 使用函数inet_ntoa()

在使用bind() 函数绑定套接字与地址前,要使用以上两类转换函数,对地址部分做好相应转换,实现代码举例如下:

((sockaddr_in*)&addr)->sin_family = AF_INET;

//AF_INET:使用Internet 地址族

((sockaddr_in*)&addr)->sin_port = htons(3000);

// htons():16位端口号3000的主机字节顺序转换成网络字节顺序((sockaddr_in*)&addr)->sin_addr.s_addr = inet_addr("210.29.174.151");

// inet_addr():主机点分十进制字符串IP地址转换为网络字节顺序的u_long IP地

bind(sock, &addr, sizeof(addr)); //把套接字与地址绑定

(4) 服务器将套接字置为监听模式

将服务器上的套接字设置为监听方式工作,使用API函数listen():

int listen(SOCKET s, int backlog);

第一个参数指定套接字。第二个backlog 参数指定了正在等待连接的最大队列长度。这个参数非常重要,因为完全可能同时出现几个服务器连接请求。例如,假定backlog 参数为2,如果三个客户机同时发出请求,那么头两个会被放在一个等待处理的队列中,以便应用程序依次为它们提供服务。而第三个连接会造成一个WSAECONNREFUSED 错误。

若无错误发生,listen 函数返回0,若失败则返回SOCKET_ERROR 错误。

(5) 服务器接受连接请求

服务器设置监听工作方式后,通过调用accept() 函数使套接字等待接受客户连接,accept() 函数的原型为:

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

其中,第一个参数s 指定套接字,它须处在监听模式。而第二个参数是一个SOCKADDR 结构形式的地址,第三个参数addrlen 为SOCKADDR 结构的长度。

如果客户端有了连接请求,服务器通过使用accept() 函数来接受客户端的请求。accept() 函数返回后,addr 结构中会包含发出连接请求的那个客户机的IP地址信息,而addrlen 参数则指出该结构的长度。此外,accept() 会返回一个新的套接字描述符,它对应于已经接受的那个客户机连接。对于该客户机后续的所有操作,都应使用这个新套接字。至于原来那个监听套接字,它仍然用于接受其他客户机连接,而且仍处于监听模式。

实现代码举例如下:

listen(sock,1); //设置服务器监听方式,1--允许等待队列的长度

len = sizeof(addr);

sersock = accept( sock, &addr, &len );

//阻塞,等待客户连接进来,从等待队列中检取客户

//接受客户连接,生成新套接字sersock对应该连接;而原监听Socket

继续等待其它客户连接请求

if(sersock = = INV ALID_SOCKET){ // 若客户连接出错,则出错返回

DWORD err = WSAGetLastError();

char txt[100];

sprintf(txt,"error when accept!---errno:%d",err);

printf(txt);

getch();

WSACleanup( );

return 1;

}

printf("有客户连接进来!\n "); // 客户连接成功

(6) 客户进程向服务进程发出连接请求

客户通过connect() 函数可以和服务器建立一个端到端的连接。connect() 函数原型为:int connect(SOCKET s, const struct sockaddr FAR* name, int namelen);

其中,参数s 指定套接字;name是对方服务器SOCKADDR 结构形式的地址;namelen 则是地址参数的长度。如果连接的服务器没有监听指定端口,connect调用就会失败,发生错误WSAECONNREFUSED。

实现代码举例如下:

// 以下输入想连接到的服务器IP 地址和服务器端口号

((sockaddr_in*)&addr)->sin_family = AF_INET; //AF_INET地址族

printf("输入服务器地址:");

gets(msg); // 输入点分十进制形式的服务器地址

((sockaddr_in*)&addr)->sin_addr.s_addr = inet_addr(msg); //地址形式转换

printf("输入服务器端口号:");

gets(msg); // 输入想连接到的服务器端口号

portno = atoi(msg); // 字符串形式端口号转换为整型

((sockaddr_in*)&addr)->sin_port = htons( portno ); //端口号转网络字节顺序

len = sizeof(addr);

printf("与服务器连接...!");

err = connect( sock, (sockaddr*)&addr, len );

if( err = = SOCKET_ERROR ){

printf("连接失败!");

getch();

WSACleanup( );

return 1;

}

printf("成功连接到服务器!\n ");

(7) 发送和接收数据,进行数据传输

对于面向连接的应用程序来说,一旦客户机与服务器实现连接,则可以进行数据传输。send()、recv() 函数是在已建连接上进行数据收发的函数。

send() 函数的原型为:

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

其中,参数s 是已建立连接的套接字,将在这个套接字上发送数据。第二个参数buf,则是字符缓冲区,区内包含即将发送的数据。第三个参数len,指定即将发送的缓冲区内的字符数。最后,flags 一般可设为0。send() 函数调用后,返回实际发送的字节数;若发生错误,就返回SOCKET_ERROR。

send() 实现代码举例如下:

send ( sock, msg, strlen(msg)+1, 0); //发送数据

recv() 函数的原型为:

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

其中,参数s 是准备接收数据的那个套接字。第二个参数buf,是即将收到数据的字符缓冲,而len 则是准备接收的字节数或buf 缓冲的长度。最后,flags参数一般可设为0。该函数调用后返回读入数据的字节数。

recv() 实现代码举例如下:

len = recv ( sersock, msg, 200, 0 ); //接收数据

对于使用无连接协议的程序,使用以下发送与接收函数:

发送无连接数据报时,可以调用sendto() 函数。

sendto() 函数的原型为:

int sendto(SOCKET s, const char FAR * buf, int len, int flags,

const struct sockaddr FAR * to, int tolen);

其中,参数s 是准备发送数据的那个套接字;第二个参数buf,是即将发送数据的字符缓冲,而len 指明发送的字节数;flags 参数一般可设为0;参数to是一个指向SOCKADDR 结构的指针、该结构中存放的是将接收数据的对方站点的目标地址;tolen 参数是该地址结构的长度。

sendto() 实现代码举例如下:

ret = sendto(sock, msg, strlen(msg)+1, 0, &recipient, sizeof(recipient));

// 发送数据报

接收无连接数据报时,可以调用recvfrom() 函数。

recvfrom() 函数的原型为:

int recvfrom(SOCKET s, char FAR * buf, int len, int flags,

struct sockaddr FAR * from, int FAR *fromlen);

其中,参数s 是准备接收数据的那个套接字;第二个参数buf,是即将接收数据的字符缓冲,而参数len 设定接收字节数;flags 参数一般可设为0;参数from 是一个指向SOCKADDR 结构的指针、该结构中存放的是对方发送站点的地址;fromlen 参数是该地址结构的长度。调用recvfrom() 后返回实际收到的字节数。

recvfrom() 实现代码举例如下:

dwSenderSize = sizeof(sender);

len = recvfrom( sock, msg, 200, 0, &sender, (int *)&dwSenderSize);

// 接收数据报在msg 中,实际接收字节数为len

(8) 关闭套接字

一旦网络通信任务完成,就必须释放套接字占用的所有资源。通常调用closesocket() 函数即可以达到目的。closesocket() 函数的原型为:

int closesocket(SOCKET s );

其中,参数s 为欲关闭的套接字。此后若再使用该套接字,调用就会失败,并出现WSAEOTSOCK 错误。

(9) 释放Winsock DLL

网络套接字应用程序结束前,要使用下面这个函数释放winsock DLL:

int WSACleanup(void);

(10) 出错信息的获取

当Winsock 函数调用发生错误时,可以通过如下函数获得错误代码:

int WSAGetLastError(void);

5.4 实验五WinSock 2.0 网络套接字编程

5.4.1 实验目的

掌握TCP/IP网络面向连接的点点通信时使用的套接字API 函数调用方法和编程工作原理,学会在Win32 Console Application方式下使用Winsock 2.0编制网络会话程序,并掌握一般网络通信程序的调试和排错方法。

5.4.2 实验准备

(1)实验设备

●网络互连设备Hub或Switch;

●装有以太网卡的PC微机,每个学生一台;

●每台PC装有软件Microsoft Windows 2000/XP,Visual C++ 6.0。

(2)预备知识

●熟悉Visual C++ 6.0 Win32 Console Application方式编程;

●熟悉程序的调试和排错方法。

●预习和熟悉网络套接字API 函数调用方法和编程工作原理;

5.4.3 实验内容

(1)建立工程,设置Visual C++ 程序的Winsock 2 网络开发环境:

进入“File”→“New”→“Projects”选项卡,选择“Win32 Console Application”,在“Project name”栏中添加工程名,在“Location”栏目设置好工程存放位置后,按“OK”,在弹出窗口中选择“An empty project”,按“Finish”、“OK”后,便建立了

一项新的工程。

再进入“File”→“New”→“Files”选项卡,选择“C++ Source File”,在“File”栏中添加文件名,后缀为.cpp,在“Location”栏目设置好.cpp源文件存放位置后,按“OK”,便将该.cpp源文件添加到工程中。

再进入“Project”→“Settings…”→“Link”选项卡下的“Object/library Modules”

中,添加静态链接函数库ws2_32.lib,Winsock 2 网络开发环境设置完毕。

(2)参考课文图5.1以及客户方与服务方Winsock API函数调用例句,在Visual C++ 6.0 Win32 Console Application方式下,使用Winsock 2套接字API编制一个面向连接的两台PC机控制台屏幕会话程序,要求两方轮流发送字符串,任一方键入的字符串显示在本方屏幕上,并传输到对方机器,显示在对方屏幕上,当双方任一方键入“bye”

后结束会话。设计出相应的服务器程序和客户端程序。

(3)在PC机上单独编辑、编译这一组网络会话程序;运行、调试服务器进程、客户进程,使用单步执行(F10键)、执行到光标位置(Ctl+F10键)等VC调试手段进行调试、排错,直至Build(编译并链接)完全无错;

(4)同组的两位同学合作,在一台PC上先运行服务端程序,再在另一台PC上运行客户端程序,两人运行、测试该网络会话程序的客户端和服务器端,记录测试结果,分析遇到的问题与解决的办法;

(5)在实验报告上记录运行后无错的客户端和服务器端控制台屏幕会话程序,总结TCP/IP 网络面向连接的套接字编程的一般编程步骤。

5.4.4 问题与思考

(1)什么是套接字?在面向连接的WinSock网络套接字编程时,服务器创建套接字后,为什么需要和自己的地址(IP地址及端口)绑定?它有何作用?客户端创建套接字后,为什么不需和自己的地址绑定?谈谈你对这个问题的思考。

(2)语句SOCKET s1 =accept(SOCKET s, struct sockaddr* addr, int* addrlen);

中有两个SOCKET,一个是函数中的参数套接字s,另一个是accept 函数返回的新套接字s1,这两个套接字描述符分别对应什么?各有什么作用?

(3)服务器如果需要同时与多个客户端建立连接、为多个客户服务,程序总的架构应如何设计?对现有程序应作什么方面的改动?谈谈你的设想。

附录:Winsock编程资料参考网址

[1] Windows网络编程技术,

https://www.360docs.net/doc/b8302488.html,/personal/csli/WinSockTech/mfc_main.htm

[2] WinSock编程规范及应用,https://www.360docs.net/doc/b8302488.html,/sort/209_1.htm

[3] 套接字通信WinSock,http://210.40.7.188/E%27ojc/Internet/021/index.html

[4] Winsock Error Reference(Winsock错误索引),

https://www.360docs.net/doc/b8302488.html,/err_lst1.htm#ErrorsInNumericOrder

[5] Winsock Development Information,https://www.360docs.net/doc/b8302488.html,/

[6] Winsock Programmer's FAQ, https://www.360docs.net/doc/b8302488.html,/wskfaq/

[7] Getting Started with Winsock, https://www.360docs.net/doc/b8302488.html,/library/default.asp?url=

/library/en-us/winsock/winsock/getting_started_with_winsock.asp

[8] Winsock Functions, https://www.360docs.net/doc/b8302488.html,/library/default.asp?url=/library/en-us

/winsock/winsock/winsock_functions.asp

[9] Winsock Structures, https://www.360docs.net/doc/b8302488.html,/library/default.asp?url=/library/en-us

/winsock/winsock/winsock_structures.asp

[10] Windows Sockets Error Codes, https://www.360docs.net/doc/b8302488.html,/library/default.asp?url=

/library/en-us/winsock/winsock/windows_sockets_error_codes_2.asp

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