糖儿飞教你学C++ Socket网络编程——5.套接字编程步骤与函数

TCP是一个面向连接的传输层协议,提供高可靠性的字节流传输服务,主要用于一次传输要交换大量报文的情形。为了维护传输的可靠性,TCP增加了许多开销:例如确认、流量控制、计时器以及连接管理等。TCP协议的传输特点是:

  1. 端到端通信:TCP的连接是端到端的,这意外着一个TCP连接只支持两方通信,通常是客户端在一端,服务器端在另一端。
  2. 建立可靠连接:TCP要求客户端在与服务器交换数据之前,必须要先连接上服务器,这样就测试了网络的连通性。
  3. 可靠交付:一旦建立连接,TCP保证数据将按发送时的顺序交付,不会丢失,也不会重复,如果因为故障而不能可靠交付,发送方会得到通知。
  4. 双工传输:在任何时候,单个TCP连接都允许同时双向传输数据,因此客户端和服务器端可同时向对方发送数据。
  5. 流模式:TCP从发送方向接收方发送的数据,是没有报文边界的字节流。

2.1.1 套接字编程步骤

要使用流式套接字开发基于TCP协议的网络通信程序,需要分别制作服务器端程序和客户端程序,这两种程序调用WinSock函数的流程如图2-1所示。

 

图2-1 TCP通信程序的WinSock函数调用流程

总的来说,TCP服务器端程序编程的步骤如下:

1)加载WinSock动态链接库 (WSAStartup());

2)创建套接字(socket()),并将第2个参数设置为SOCK_STREAM;

3)绑定套接字(bind())到一个IP地址和端口上;

4)将套接字设置为监听模式等待连接请求(listen()),套接字监听就相当于手机待机,要等待客户端连接,必须提前处于监听状态,才能保证客户端任何时刻都能连接上;

5)请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

6)用返回的套接字和客户端进行通信(send()和recv());

7)返回,等待另一连接请求;

8)关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

TCP客户端程序编程的步骤如下:

1)加载WinSock动态链接库(WSAStartup());

2)创建套接字(socket()),第2个参数设置为SOCK_STREAM;

3)向服务器发送连接请求(connect());

4)和服务器端进行通信(send()/recv());

5)关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())。

其中,需要注意几点:

1) accept是等待接受连接函数,因此在TCP通信中,是服务器端先执行accept函数,客户端再接着执行connect函数。但connect函数会先于accept函数执行完毕。如果connect函数连接成功,则accept函数会返回一个新的套接字。

2) 在TCP通信中,任何一方都可以先发数据给对方,因此send()和recv()函数是没有先后顺序之分的。

3)服务器端程序需要先运行,客户端程序才能运行。因为只有服务器处于监听状态(listen)时,客户端才能成功发送连接(connect)请求。

2.1.2 套接字编程的准备工作

WinSock由两部分组成:开发组件和运行组件。开发组件主要是WinSock的头文件:winsock2.h,头文件包括了WinSock实现所定义的宏、常数值、结构体和函数调用接口原型。运行组件是指WinSock应用程序接口的动态链接库(DLL)和静态链接库(导入库)文件,WinSock各版本的头文件和链接库文件如表2-1所示。

表2-1 WinSock各版本的头文件和链接库

版本

头文件

静态链接库文件

动态链接库文件

WinSock 1

winsock.h

winsock.lib

winsock.dll

WinSock 2

winsock2.h

ws2_32.lib

ws2_32.dll

要在VC++编程中使用WinSock,必须做下面几步的准备工作。

1. 包含WinSock头文件

包含WinSock的头文件需要在程序文件首部使用编译预处理命令“#include”,将WinSock头文件包含进来。例如下面的预处理命令将把winsock2.h文件包含进来。

#include <winsock2.h>

提示:winsock2.h头文件与windows.h头文件存在相互包含关系,因此,如果在程序中已包含了windows.h文件,就不必再包含winsock2.h文件了。如果要包含winsock2.h文件,则一定要将#include <winsock2.h>写在#include<windows.h>之前。

2.链接WinSock导入库

链接WinSock导入库,有两种方法:

第一种是在程序中使用预处理命令“#pragma comment”。例如,程序要使用WinSock2时,可使用如下预处理命令:

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

第二种方法是在VC6.0的“工程→设置”菜单中,选择“连接”选项卡,在如图2-2所示的对话框中的“对象/库模块”下输入“Ws2_32.lib”。如果是VS2010,则在项目属性页中的“配置属性→链接器→输入”的“附加依赖项”中直接添加导入库名字。

 

图2-2 在VC6.0中链接导入库

由于第二种方法在不同的系统中需要重新设置,而第一种方法方便代码共享,因此建议使用第一种方法链接WinSock库。

2.1.3 套接字编程中使用的函数

下面对套接字通信中使用的各个函数进行详细介绍。

1. WSAStartup()函数

应用程序运行时必须先载入WinSock动态链接库(ws2_32.dll)才能调用WinSock函数实现网络通信功能。加载动态链接库的方法是使用WSAStartup()函数,该函数原型如下:

int WSAStartup (

    WORD   wVersionRequested,    //版本号

    LPWSADATA   lpWSAData    //一个指向WSADATD结构体变量的指针

 );

该函数返回值是一个整数,函数调用成功则返回0。

假如一个程序要使用2.2版本的Winsock,那么程序中可采用如下代码加载Winsock动态链接库:

WSADATD wsaData;

int err = WSAStartup(MAKEWORD( 2, 2 ), &wsaData );

if(err!=0){

       cout<<"Winsock不能被初始化!";      //Winsock初始化错误处理代码

       WSACleanup();      }

提示:MAKEWORD()是一个宏定义(注意不是函数),它的作用是把2个字节型数据合成一个WORD型(16位整型)数据。

2. socket()函数

在WinSock中,socket()函数用来创建套接字,其函数原型如下:

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

该函数有3个参数,各参数的含义如下:

  1. af:标识一个地址家族,在Windows中总是为AF_INET。
  2. type:表示套接字的类型,取值有3种:SOCK_STREAM表示流式套字;SOCK_DGRAM表示数据报套接字;SOCK_RAW表示原始套接字。
  3. protocol:用于指定套接字所用的特定协议,依赖于第2个参数type,对于TCP或UDP通信来说,该参数一般设为0,表示默认的协议;但对于原始套接字来说,该参数有很多不同的取值。

socket()函数的返回值数据类型是SOCKET,它是Winsock中专门定义的一种新的数据类型,表示套接字描述符,是一个无符号整型数。其定义为:

typedef u_int SOCKET;

3. bind()函数

socket()函数在创建套接字时并没有为创建的套接字分配地址,因此服务器端在创建了监听套接字之后,需要使用bind()函数将套接字绑定到一个已知的地址上,即为套接字指定协议名、本机IP地址和端口号。该函数原型为:

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

该函数有3个参数,各个参数的含义如下:

  1. s:需要绑定到的套接字。
  2. name:是一个sockaddr结构指针,该结构中包含了要绑定的地址和端口号。
  3. namelen:是name缓冲区的长度。

如果函数执行成功,则返回值为0,否则为SOCKET_ERROR。

提示:

客户端的套接字一般不用绑定地址,当客户端程序调用connect()函数与服务器建立连接时,系统会自动为套接字选择一个IP地址和临时端口号,因此客户端很少使用bind()函数。

服务器端的监听套接字不绑定地址也不会出现明显错误,因为当服务器调用listen()函数时,系统也会为套接字分配IP地址和临时TCP端口号,不过由于临时端口号很难被客户端知晓从而导致客户端无法连接服务器,因此服务器端需要用bind()函数绑定地址。

4. listen()函数

listen()函数是只能由服务器端使用的函数,而且只适用于流式套接字,listen()函数用于将套接字设置为监听模式。该函数原型为:

int listen (SOCKET s, int backlog);

该函数有2个参数,各个参数的含义如下:

  1. s:套接字。
  2. backlog:表示等待连接的最大队列长度。例如,若设置backlog为4,当同时收到5个客户端连接请求时,则前4个客户端连接请求会放置在等待队列中,第5个客户端会得到错误信息。该参数值通常设置为常量SOMAXCONN,表示将连接等待队列的最大长度值设为一个最大的“合理”值,该值由底层开发者指定,在WinSock2中,该值为5。

提示:listen()函数中的backlog设置的是等待连接的客户端的最大个数,并不是服务器端能够同时连接的客户端数。TCP是一对一通信协议,因此一个服务器套接字在任何时候都只能连接一个客户端。

5. accpet()函数

accept()函数只适用于流式套接字,并且也是只能由服务器端使用的函数,其功能是接收指定的监听套接字上传入的一个连接请求,并尝试与请求方建立连接,连接建立成功后则返回为该连接创建的一个新套接字。该函数原型为:

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

该函数有3个参数,各个参数的含义如下:

  1. s:是一个套接字,它应处于监听状态。
  2. addr:是一个sockaddr类型的结构指针,包含一组客户端的IP地址、端口号等信息。
  3. addrlen:指针类型,指向参数addr的长度。

accept()函数返回一个已建立连接的新的套接字的描述符(即已连接套接字的描述符),服务器与客户端的所有后续通信,都应使用该新的套接字(称为通信套接字)。而原来的监听套接字仍然处于监听状态,可以继续接受其他客户端的连接请求。

默认情况下,如果调用accept()函数时还没有客户端的连接请求到来,accept()函数将继续等待,进程将阻塞,直到客户端与服务器建立了连接之后才会返回。

6. connect()函数

connect()函数只能用在客户端,其功能是建立客户端与服务器之间的连接。客户端调用connect函数时发起主动连接,TCP协议开始三次握手过程,三次握手过程完成后,connect()函数返回。该函数的原型如下:

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

各个参数的含义如下:

  1. s:标识一个套接字。
  2. name:套接字s想要连接的主机地址和端口号。
  3. namelen:name缓冲区的长度。

connect函数用于发送一个连接请求。若成功则返回0,否则为SOCKET_ERROR。用户可以通过WSAGetLastError得到其错误描述。

7. send()函数

在连接建立成功后,就可以在已建立连接的套接字上发送和接收数据了。对于流式套接字,发送数据通常使用send()函数。注意send()函数发送成功仅表示已经将数据发送到了本机WinSock的缓冲区中,并不表示对方主机已成功接收。该函数的原型为:

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

该函数有4个参数,各个参数的含义如下:

  1. s: 已建立连接的套接字标识符;
  2. buf :用来存放待发送数据的缓冲区,是该缓冲区地址的指针,如字符数组名;
  3. len:缓冲区buf中要发送数据的字节数,如strlen(str)+1;
  4. flags :用于控制数据发送的方式,通常取0,表示正常发送数据;如果取值为宏MSG_DONTROUT,则表示目标主机就在本地网络中,也就是与本机在同一个IP网段上,数据分组无须路由即可直接交付目的主机,如果传输协议的实现不支持该选项则忽略该标志;如果取值为宏MSG_OOB,则表示数据将按带外数据发送。

该函数的返回值是成功发送的字节数,注意该发送的字节数有可能小于参数len,如果连接已关闭则返回0,若发送错误,则返回SOCKET_ERROR。

8. recv()函数

recv()函数用来在已建立连接的流式套接字中接收数据,该函数实际上仅从本机的WinSock缓冲区中读取数据。该函数执行成功则返回实际从套接字s读入到buf中的字节数。连接终止则返回0;否则返回SOCKET_ERROR错误号,该函数的原型如下:

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

该函数有4个参数,各个参数的含义如下:

  1. s:已建立连接的套接字标识符;
  2. buf:是接收数据的缓冲区,是该缓冲区地址的指针;
  3. len:是buf的长度,如sizeof(buf)。
  4. flags:表示函数的调用方式,一般取值为0。

9. closesocket()函数

网络通信完成后,程序退出前应使用closesocket()函数关闭套接字以释放资源,此外,closesocket()还会发送数据包导致TCP通信的连接断开,该函数的原型如下:

int closesocket (SOCKET s );

该函数的参数为一个要被关闭的套接字,如果执行成功则返回0,否则返回SOCKET_ERROR。

10. WSACleanup()函数

应用程序在完成对WinSock动态链接库的使用后,需要注销与WinSock库的绑定,以释放WinSock库所占用的系统资源。WSACleanup()函数用来注销WinSock动态链接库。该函数的原型为:

int WSACleanup (void);

该函数无参数,执行成功后将返回0,否则返回SOCKET_ERROR。对应于应用程序中每一次对WSAStartup()调用,都应该有一个WSACleanup()的调用。

2.1.4 套接字建立连接与TCP三次握手

服务器端在调用listen()函数之后,内核会建立两个队列,SYN队列和ACCEPT队列,其中ACCPET队列的长度由backlog值指定。

TCP套接字建立连接的过程与TCP三次握手的关系如图2-3所示,步骤如下:

1)服务器端在调用accpet()函数之后,将阻塞,等待ACCEPT队列中有元素。

2)客户端在调用connect()函数之后,将开始发起SYN请求,请求与服务器建立连接,此时称为第一次握手。

3)服务器端在接受到SYN请求之后,把请求方放入SYN队列中,并给客户端回复一个确认帧ACK,此帧还会携带一个请求与客户端建立连接的请求标志,也就是SYN,这称为第二次握手。

4)客户端收到SYN+ACK帧后,connect()函数将返回,并发送确认建立连接帧ACK给服务器端,这称为第三次握手。

5)服务器端收到ACK帧后,会把请求方从SYN队列中移出,放至ACCEPT队列中,而accept函数也等到了自己的资源,从阻塞中唤醒,从ACCEPT队列中取出请求方,重新建立一个新的套接字,并返回。

这就是listen(),accept(),connect()三个函数的工作流程及原理。从这个过程可以看到,在connect函数中发生了两次握手。

 

 

图2-3 套接字建立连接与TCP三次握手的关系

2.3 UNIX Socket编程

目前,虽然Windows是最流行的个人PC操作系统,但大多数企业的服务器使用的却是Unix或Linux等开源的操作系统。因此很多C/S模式的网络软件,其服务器端为了便于部署到Unix系统上,一般采用BSD Socket开发,而客户端则采用WinSock开发。

BSD Socket和WinSock从总体上看是很相似的,开发者只要掌握了WinSock,再了解一下两者的区别就能快速地掌握BSD Socket。BSD Socket和WinSock的主要区别如下。

(1)BSD Socket不需要初始化协议栈

由于Unix操作系统将BSD Socket的运行库已集成到操作系统的内核中,因此操作系统启动时就已经加载了Socket协议栈。所以在BSD Socket编程中,没有初始化协议栈和清空协议栈的步骤,也就不需要在程序中使用WSAStartup()和WSACleanup()这两个函数。

(2)BSD Socket中的某些套接字函数名与WinSock中的不同

在WinSock中,只能使用套接字I/O函数recv()和send()函数进行消息的收发,而在BSD Socket中一般使用文件I/O函数read()和write()函数进行消息的收发,当然,BSD Socket中也可使用recv()和send()函数,只是不常用。

BSD Socket中关闭套接字的函数是close(),对应WinSock中的closesocket()函数。

(3)套接字的语法存在区别

在WinSock中,socket()和accept()函数的返回值是一个SOCKET类型,该类型用来保存整数型的套接字句柄值,而BSD Socket中,socket()和accept()函数的返回值却是一个int整型数,如果执行出错则返回-1。

(4)套接字引用的头文件不同

在BSD Socket编程中,需要引用以下几个头文件:

  #include <sys/types.h>                     // 基本系统数据类型

  #include <sys/socket.h>            // socket 核心函数和数据结构

  #include <netinet/in.h>              // AF_INET地址家族和对应的协议家族

  #include <arpa/inet.h>               // 和IP地址相关的一些函数

习题

1. 在VC中使用WinSock 2.2进行编程,需要引用的头文件是                    (             )

A. winsock.h         B. winsock2.h               C. winsock22.h                    D. ws2_32.h

2. 关于MAKEWORD(),下列说法中正确的是:                  (       )

A. 是一个函数                            B. 是一个运算符 

C. 是一个宏定义                        D. 功能是将两个整型数合并成一个WORD型

3. 下列哪个函数只能用在客户端程序中                                       (           )

A. bind()                       B. connect()                 C. recv()               D. listen()

4. 在WinSock中,TCP通信中bind()函数绑定的地址是(      )

A. 本机地址                               B. 远程地址

C. 服务器端绑定的是本机地址,客户端绑定的远程地址

D. 服务器端绑定的是远程地址,客户端绑定的本机地址

5. bind()函数要求的地址类型是                                                    (      )

A. sockaddr_in               B. sockaddr           C. in_addr                    D. inet_addr

6. bind()函数第2个参数的正确写法是                                          (             )

A. (SOCKADDR)&addrSer                         B. (SOCKADDR*)addrSer

C. (SOCKADDR*)&addrSer                       D. (SOCKADDR)addrSer

7. 以下哪一项不是结构体数据类型                                       (             )

A. WSADATA                B. SOCKET                  C. sockaddr           D.sockaddr_in

8. 如果客户端执行了closesocket()函数关闭套接字,服务器端再执行recv()函数,则recv()函数的返回值是                                                                                    (             )

A. 1                              B. 0                             C. -1                     D.不会返回值

9. socket()、bind()、listen()、accept()、connect()、send()、WSAStartup()函数的参数个数分别为                                              (           )

A. 3 3 2 3 3 4 2                     B. 3 2 2 3 4 4 2             C. 3 4 2 3 3 4 2             D. 3 3 3 3 3 3 2

10. 要将TCP端口号由网络字节顺序转换为主机字节顺序,应使用哪个函数(        )

A. htons()                      B. htonl()                      C.ntohl()               D. ntohs()

11.(多选题)listen()函数参数中的套接字和                   函数参数中的套接字是同一个套接字                                                                                                         (           )

A. bind                          B. recv                         C. send                 D. accept

12. 如果要创建一个流式套接字,则代码为socket(AF_INET,                     ,0)。

13. 要获取套接字地址的长度,一般使用                       运算符。

13. socket()函数的返回值和accept()函数的返回值是同一个套接字吗?

14. 在TCP通信中,为什么服务器端需建立两个套接字,而客户端只需要建立一个套接字?

15. (实验)默写2.3节的程序,要求去掉所有错误处理代码。

16. (实验)在2.3节的程序中,服务器端只能接受一次客户端的连接,如果希望客户端关闭后,重新启动客户端,仍然能连接上服务器,应该怎样修改程序呢?

17. (实验)改写2.3节的程序,制作一个回声程序,即客户端发送一个消息给服务器端后,服务器将自动发送相同的消息给客户端,并显示消息长度。例如:

客户端:> 新年好

服务器端:> 收到消息“新年好”,共7个字节。

 

  • 5
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值