《Windows网络与通信程序设计(第3版)》——2.3 Winsock编程详解

2.3 Winsock编程详解

使用TCP创建网络应用程序稍微复杂一些,因为TCP是面向连接的协议,需要通信双方首先建立一个连接。本节先以建立简单的TCP客户端和服务器端应用程序为例,详细说明Winsock的编程流程,然后再介绍较为简单的UDP编程。

2.3.1 Winsock编程流程
使用Winsock编程的一般步骤是比较固定的,可以结合后面的例子程序来理解它们。

1.套接字的创建和关闭
使用套接字之前,必须调用socket函数创建一个套接字对象,此函数调用成功将返回套接字句柄。

SOCKET     socket(
        int af,                          // 用来指定套接字使用的地址格式,WinSock中只支持AF_INET
        int type,                     // 用来指定套接字的类型
        int protocol                // 配合type参数使用,用来指定使用的协议类型。可以是IPPROTO_TCP等
        );

type参数用来指定套接字的类型。套接字有流套接字、数据报套接字和原始套接字等,下面是常见的几种套接字类型定义。

SOCK_STREAM:流套接字,使用TCP提供有连接的可靠的传输
SOCK_DGRAM:数据报套接字,使用UDP提供无连接的不可靠的传输
SOCK_RAW:原始套接字,Winsock接口并不使用某种特定的协议去封装它,而是由程序自行处理数据报以及协议首部。
当type参数指定为SOCK_STREAM和SOCK_DGRAM时,系统已经明确使用TCP和UDP来工作,所以protocol参数可以指定为0。

函数执行失败返回INVALID_SOCKET(即-1),可以通过调用WSAGetLastError取得错误代码。

也可以使用Winsock2的新函数WSASocket来创建套接字,与socket相比,它提供了更多的参数,如可以自己选择下层服务提供者、设置重叠标志等,后面再具体讨论它。

当不使用socket创建的套接字时,应该调用closesocket函数将它关闭。如果没有错误发生,函数返回0,否则返回SOCKET_ERROR。函数用法如下。

int closesocket(SOCKET s);     // 函数唯一的参数就是要关闭的套接字的句柄

2.绑定套接字到指定的IP地址和端口号
为套接字关联本地地址的函数是bind,用法如下。

int bind(
    SOCKET s,                                         // 套接字句柄
    const struct sockaddr* name,            // 要关联的本地地址
    int namelen                                        // 地址的长度
);

bind函数用在没有建立连接的套接字上,它的作用是绑定面向连接的或者无连接的套接字。套接字被socket函数创建以后,存在于指定的地址家族里,但它是未命名的。bind函数通过安排一个本地名称到未命名的socket而建立此socket的本地关联。本地名称包含3部分:主机地址、协议号(分别为UDP或TCP)和端口号。

本节的TCPServer程序使用以下代码绑定套接字s到本地地址。

// 填充sockaddr_in结构
          sockaddr_in sin;
          sin.sin_family = AF_INET;
          sin.sin_port = htons(4567);
          sin.sin_addr.S_un.S_addr = INADDR_ANY;
          // 绑定这个套接字到一个本地地址
          if(::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
          {
                    printf("Failed bind() \n");
                    return 0;
          }

sockaddr_in结构中的sin_familly字段用来指定地址家族,该字段和socket函数中的af参数的含义相同,所以唯一可以使用的值就是AF_INET。sin_port字段和sin_addr字段分别指定套接字需要绑定的端口号和IP地址。放入这两个字段的数据的字节顺序必须是网络字节顺序。因为网络字节顺序和Intel CPU的字节顺序刚好相反,所以必须首先使用htons函数进行转换。

如果应用程序不关心所使用的地址,可以指定Internet地址为INADDR_ANY,指定端口号为0。如果Internet地址等于INADDR_ANY,系统会自动使用当前主机配置的所有IP地址,简化了程序设计;如果端口号等于0,程序执行时系统会为这个应用程序分配唯一的端口号,其值在1024~5000之间。应用程序可以在bind之后使用getsockname来知道为它分配的地址。但是要注意,直到套接字连接上之后getsockname才可能填写Internet地址,因为对一个主机来说可能有多个地址是可用的。

TCP客户端程序也可以在不显式绑定地址和端口号的情况下发送数据或者连接。在这种情况下,系统也会默认地为套接字绑定一个本地端口(1024~5000之间)。

3.设置套接字进入监听状态
listen函数设置套接字进入监听状态。

int listen(
    SOCKET s,                  // 套接字句柄
    int backlog                    // 监听队列中允许保持的尚未处理的最大连接数量
);

为了接受连接,首先使用socket函数创建套接字,然后使用bind函数将它绑定到本地地址,再用listen函数为到达的连接指定backlog,最后使用accept接受请求的连接。

listen仅应用在支持连接的套接字上,如SOCK_STREAM类型的套接字。函数执行成功后,套接字s进入了被动模式,到来的连接会被通知要排队等候接受处理。

在同一时间处理多个连接请求的服务器通常使用listen函数,如果一个连接请求到达,并且排队已满,客户端将接收到WSAECONNREFUSED错误。

4.接受连接请求
accept函数用于接受到来的连接。

SOCKET accept(
    SOCKET s,                              // 套接字句柄
    struct sockaddr* addr,             // 一个指向sockaddr_in结构的指针,用于取得对方的地址信息
    int* addrlen                             // 一个指向地址长度的指针
);

该函数在 s 上取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄。新创建的套接字是处理实际连接的套接字,它与s有相同的属性。

程序默认工作在阻塞模式下,这种方式下如果没有未处理的连接存在,accept函数会一直等待下去,直到有新的连接发生才返回。

addrlen参数用于指定addr所指空间的大小,也用于返回地址的实际长度。如果addr或者addrlen是NULL,则没有关于远程地址的信息返回。

客户端程序在创建套接字之后,要使用connect函数请求与服务器连接,函数原型如下。

Int      connect(
        SOCKET s,                    // 套接字句柄
        const struct sockaddr FAR * name, // 一个指向 sockaddr_in 结构的指针,包含了要连接的服务器的地址信息
        int namelen                     // sockaddr_in结构的长度
        );

第一个参数s是此连接使用的客户端套接字,另两个参数name和namelen用来寻址远程套接字(正在监听的服务器套接字)。

5.收发数据
对流套接字来说,一般使用send和recv函数来收发数据。

int     send(
        SOCKET s,                                // 套接字句柄
        const char FAR * buf,                  // 要发送数据的缓冲区地址
        int len,                                    // 缓冲区长度
        int flags                                        // 指定了调用方式,通常设为0
        );
int    recv( SOCKET s, char FAR * buf, int len, int );

send函数在一个连接的套接字上发送缓冲区内的数据,返回发送数据的实际字节数。recv函数从对方接收数据,并将其存储到指定的缓冲区。flags参数在这两个函数中通常设为0。

在阻塞模式下,send将会阻塞线程的执行直到所有的数据发送完毕(或者发生错误),而recv函数将返回尽可能多的当前可用信息,直到达到缓冲区指定的大小。

2.3.2 典型过程图
TCP服务器程序和客户程序的创建过程如图2.2所示。服务器端创建监听套接字,并为它关联一个本地地址(指定IP地址和端口号),然后进入监听状态准备接受客户的连接请求。为了接受客户端的连接请求,服务器端必须调用accept函数。
客户端创建套接字后即可调用connect函数去试图连接服务器监听套接字。当服务器端的accept函数返回后,connect函数也返回。此时客户端使用socket函数创建了套接字,服务器端使用accept函数创建了套接字,双方就可以通信了。

2.3.3 TCP服务器和客户端程序举例
下面是最简单的TCP服务器程序和TCP客户端程序的例子。这两个程序都是控制台界面的Win32应用程序,分别在配套光盘的TCPServer和TCPClient工程下。

运行服务器程序TCPServer,如果没有错误发生,将在本地机器上的4567端口上等待客户端的连接。如果没有连接请求,服务器会一直处于休眠状态。

运行服务器之后,再运行客户端程序TCPClient,其最终效果如图2.3所示。客户端连接到了服务器,双方套接字可以通信了。

下面是TCPServer程序源代码。

#include "../common/InitSock.h"
#include <stdio.h>
CInitSock initSock;          // 初始化Winsock库
int main()
{
          // 创建套接字
          SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
          if(sListen == INVALID_SOCKET)
          {       printf("Failed socket() \n");
                    return 0;
          }
          // 填充sockaddr_in结构
          sockaddr_in sin;
          sin.sin_family = AF_INET;
          sin.sin_port = htons(4567);
          sin.sin_addr.S_un.S_addr = INADDR_ANY;
          // 绑定这个套接字到一个本地地址
          if(::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
          {
                    printf("Failed bind() \n");
                    return 0;
          }
          // 进入监听模式
          if(::listen(sListen, 2) == SOCKET_ERROR)
          {
                    printf("Failed listen() \n");
                    return 0;
          }
          // 循环接受客户的连接请求
          sockaddr_in remoteAddr;
          int nAddrLen = sizeof(remoteAddr);
          SOCKET sClient;
          char szText[] = " TCP Server Demo! \r\n";
          while(TRUE)
          {        // 接受新连接
                    sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);
                    if(sClient == INVALID_SOCKET)
                    {
                              printf("Failed accept()");
                              continue;
                    }
                    printf(" 接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));
                    // 向客户端发送数据
                    ::send(sClient, szText, strlen(szText), 0);
                    // 关闭同客户端的连接
                    ::closesocket(sClient);
          }
          // 关闭监听套接字
          ::closesocket(sListen);
          return 0;
}

下面是TCPClient程序源代码。

#include "../common/InitSock.h"
#include <stdio.h>
CInitSock initSock;                    // 初始化Winsock库
int main()
{
          // 创建套接字
          SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
          if(s == INVALID_SOCKET)
          {        printf("Failed socket() \n");
                    return 0;
          }
          // 也可以在这里调用bind函数绑定一个本地地址
          // 否则系统将会自动安排
          ...
          // 填写远程地址信息
          sockaddr_in servAddr;
          servAddr.sin_family = AF_INET;
          servAddr.sin_port = htons(4567);
          // 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
          // 如果你的计算机没有联网,直接使用127.0.0.1即可
          servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
          if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
          {        printf("Failed connect() \n");
                    return 0;
          }
          // 接收数据
          char buff[256];
          int nRecv = ::recv(s, buff, 256, 0);
          if(nRecv > 0)
          {
                    buff[nRecv] = '\0';
                    printf(" 接收到数据:%s", buff);
          }
          // 关闭套接字
          ::closesocket(s);
          return 0;
}

2.3.4 UDP编程
TCP由于可靠、稳定的特点而被用在大部分场合,但它对系统资源要求比较高。UDP是一个简单的面向数据报的传输层协议,又叫用户数据报协议。它提供了无连接的、不可靠的数据传输服务。无连接是指它不像TCP那样在通信前先与对方建立连接以确定对方的状态。不可靠是指它直接按照指定IP地址和端口号将数据包发出去,如果对方不在线的话数据可能丢失。

1.UDP编程流程
(1)服务器端程序设计流程如下。

① 创建套接字(socket)。

② 绑定IP地址和端口(bind)。

③ 收发数据(sendto/recvfrom)。

④ 关闭连接(closesocket)。

(2)客户端程序设计流程如下。

① 创建套接字(socket)。

② 收发数据(sendto/recvfrom)。

③ 关闭连接(closesocket)。

UDP用于发送和接收数据的函数是sendto和recvfrom,它们的用法如下。

int sendto (
        SOCKET s,                                                  // 用来发送数据的套接字
        const char FAR * buf,                                 // 指向发送数据的缓冲区
        int len,                                                           // 要发送数据的长度
        int flags,                                                        // 一般指定为0
        const struct sockaddr * to,                          // 指向一个包含目标地址和端口号的sockaddr_in结构
        int tolen                                                        // 为sockaddr_in结构的大小
      );

同样,UDP接收数据时也需要知道通信对端的地址信息。

int recvfrom (SOCKET s, char FAR buf, int len, int flags, struct sockaddr FAR from, int FAR* fromlen);

这个函数比recv函数多出最后两个参数,from 参数是指向sockaddr_in结构的指针,函数在这里返回数据发送方的地址,fromlen参数用于返回前面的sockaddr_in结构的长度。

2.UDP编程举例
下面是一个最简单的UDP服务器程序UDPServer。它运行之后,进入无限循环,监听4567端口到来的UDP封包,如果发现就将用户数据以字符串形式打印出来。相关代码如下。

// 创建套接字
// 完整代码在UDPServer工程下
          SOCKET s = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
          if(s == INVALID_SOCKET)
          {
                    printf("Failed socket() \n");
                    return 0;
          }
          // 填充sockaddr_in结构
          sockaddr_in sin;
          sin.sin_family = AF_INET;
          sin.sin_port = htons(4567);
          sin.sin_addr.S_un.S_addr = INADDR_ANY;

          // 绑定这个套接字到一个本地地址
          if(::bind(s, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)
          {        printf("Failed bind() \n");
                    return 0;
          }
          // 接收数据
          char buff[1024];
          sockaddr_in addr;
          int nLen = sizeof(addr);
          while(TRUE)
          {
                    int nRecv = ::recvfrom(s, buff, 1024, 0, (sockaddr*)&addr, &nLen);
                    if(nRecv > 0)
                    {
                              buff[nRecv] = '\0';
                              printf(" 接收到数据(%s):%s", ::inet_ntoa(addr.sin_addr), buff);
                    }
          }
          ::closesocket(s);

客户端程序更简单,创建套接字之后,调用sendto即可向指定地址发送数据。本例中相关代码如下。

// 填写远程地址信息                                                                                // 完整代码在UDPClient工程下
          sockaddr_in addr;
          addr.sin_family = AF_INET;
          addr.sin_port = htons(4567);
          addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
          // 发送数据
          char szText[] = " TCP Server Demo! \r\n";
          ::sendto(s, szText, strlen(szText), 0, (sockaddr*)&addr, sizeof(addr));

值得注意的是,创建套接字之后,如果首先调用的是sendto函数,则可以不调用bind函数显式地绑定本地地址,系统会自动为程序绑定,因此今后即便是调用recvfrom也不会失败(因为套接字已经绑定了)。但是,如果创建套接字之后,直接调用recvfrom就会失败,因为套接字还没有绑定。

时间: 2024-09-13 22:59:36

《Windows网络与通信程序设计(第3版)》——2.3 Winsock编程详解的相关文章

《Windows网络与通信程序设计(第3版)》——1.4 网络应用程序设计基础

1.4 网络应用程序设计基础 本节讲述网络应用程序设计的原则和网络程序开发环境的设置. 1.4.1 网络程序体系结构在创建网络应用程序之前,首先要决定应用程序的体系结构.应用程序体系结构(application architecture)由应用程序开发者设计,它指定了在各种各样的终端系统上,应用程序是如何组织的.本节介绍现有的主要体系结构:客户机/服务器体系结构.P2P体系结构和这两种结构的混合. 1.客户机/服务器体系结构在客户机/服务器体系结构中,有一个总是在运行的主机,称为服务器,它为来自

《Windows网络与通信程序设计(第3版)》——第2章 Winsock编程接口2.1 Winsock库

第2章 Winsock编程接口 Winsock是Windows下网络编程的标准接口,它允许两个或多个应用程序在相同机器上,或者是通过网络相互交流.Winsock是真正的协议无关的接口,本章主要讲述如何使用它来编写应用层的网络应用程序. 2.1 Winsock库 Winsock库有两个版本,Winsock1和Winsock2.现在开发网络应用程序都使用Winsock2,需要在程序中包含头文件winsock2.h,它包含了绝大部分socket函数和相关结构类型的声明和定义.同时要添加的还有到WS2_

《Windows网络与通信程序设计(第3版)》——1.2 计算机网络参考模型

1.2 计算机网络参考模型 了解网络的相关概念之后,本节将讨论计算机网络中主机之间是如何进行通信的,以及各种通信协议之间的关系等. 1.2.1 协议层次为了降低设计难度,大部分网络都以层(layer或level)的形式组织在一起,每一层都建立在它的下层之上,使用它的下层提供的服务,下层对它的上层隐藏了服务实现的细节.这种方法几乎应用于整个计算机科学领域,也可以称为信息隐藏.数据类型抽象.数据封装.面向对象编程等. 一个机器上的第n层和另一个机器的第n层交流,所使用的规则和协定合起来称为第n层协议

《Windows网络与通信程序设计(第3版)》——2.2 Winsock的寻址方式和字节顺序

2.2 Winsock的寻址方式和字节顺序 本节讲述在Winsock中主机地址信息的表示方法,以及相关的操作函数. 2.2.1 Winsock寻址 因为Winsock要兼容多个协议,所以必须使用通用的寻址方式.TCP/IP使用IP地址和端口号来指定一个地址,但是其他协议也许采用不同的形式.如果Winsock强迫使用特定的寻址方式,添加其他协议就不大可能了.Winsock的第一个版本使用sockaddr结构来解决此问题. struct sockaddr { u_short sa_family; c

《Windows网络与通信程序设计(第3版)》——第1章 计算机网络基础1.1 网络的概念和网络的组成

第1章 计算机网络基础 本章详细讲述网络程序设计中要用到的计算机网络方面的基础知识,包括各种网络术语.网络硬件设备.网络拓扑结构.网络协议等. 1.1 网络的概念和网络的组成 网络是各种连在一起的可以相互通信的设备的集合.本书讲述的网络是最常见的,将数亿计算机连接到一起的Internet.下面通过讲述组成Internet的基本硬件和软件来进一步明确计算机网络的概念. Internet是世界范围内的计算机网络,它不仅连接了PC.存储和传输信息的服务器,还连接了PDA.电视.移动PC等.所有的这些设

《Windows网络与通信程序设计(第3版)》——2.4 网络对时程序实例

2.4 网络对时程序实例 网络对时也就是从Internet上获得准确的时间,以此来校对本地计算机时钟.通过这样一个实例程序,大家可以初步了解协议和Winsock函数的具体应用. ** 2.4.1 时间协议(Time Protocol)** Time Protocol (RFC-868)是一种非常简单的应用层协议.它返回一个未格式化的32位二进制数字,这个数字描述了从1900年1月1日午夜到现在的秒数.服务器在端口37监听时间协议请求,以TCP/IP或者UDP/IP格式返回响应.将服务器的返回值转

《Windows网络与通信程序设计(第3版)》——1.3 网络程序寻址方式

1.3 网络程序寻址方式 编写网络程序,必须要有一种机制来标识通信的双方.本节详细讨论Internet中各层的寻址方式,以及相关的寻址协议. 1.3.1 MAC地址网络通信的最边缘便是LAN了,我们先来看看在LAN中是如何寻址的. 1.MAC子层和MAC地址LAN主要使用广播通信.在其内部,许多主机连在相同的通信通道上,通信时的关键问题是当竞争存在时如何决定谁使用通道.解决此问题的协议属于链路层的子层,称为MAC(Medium Access Control,介质访问控制)子层.MAC子层在LAN

java-Java语言程序设计第八版基础篇的编程练习

问题描述 Java语言程序设计第八版基础篇的编程练习 为什么按照下面的运行实例的数进行输入所得的结果却不同import java.util.Scanner; public class FutureInvestmentValue { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.print(""Enter investment amount:"

[推荐]ORACLE PL/SQL编程详解之一:PL/SQL 程序设计简介(千里之行,始于足下)

原文:[推荐]ORACLE PL/SQL编程详解之一:PL/SQL 程序设计简介(千里之行,始于足下) [推荐]ORACLE PL/SQL编程详解之一: PL/SQL 程序设计简介(千里之行,始于足下) --通过知识共享树立个人品牌. 继上六篇:        [顶]ORACLE PL/SQL编程详解之二:PL/SQL块结构和组成元素(为山九仞,岂一日之功)        [推荐]ORACLE PL/SQL编程详解之三:PL/SQL流程控制语句(不给规则,不成方圆)        [推荐]ORA