糖儿飞教你学C++ Socket网络编程——6.控制台版的TCP通信程序

根据图2-1的TCP通信程序的流程,下面编程实现一个控制台版的TCP通信程序,程序分为服务器端和客户端,双方可以相互发送消息,运行效果如图2-4所示。

 

图2-4 控制台版的TCP通信程序(左图为服务器端,右图为客户端)

2.2.1服务器端程序的制作

服务器端程序的制作步骤如下:

1. 在VC6中新建工程,选择“Win32 Console Application”,输入工程名(如TCPServer),在“下一步”中选择“一个简单的程序”。

2. 在工作空间的左侧的FileView工作区中,双击Source Files下面的“工程名.cpp”的源文件,输入如下代码,编译,运行,运行结果如图2-4所示。

#include "stdafx.h"

#include<iostream.h>

#include<winsock2.h>

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

int main(){      

WSADATA wsaData;    

if(WSAStartup(MAKEWORD(2,2), &wsaData))        //初始化WinSock协议栈

{     cout<<"Winsock不能被初始化!";

        WSACleanup();

        return 0;  }    

SOCKET sockSer, sockConn;                    //注意服务器端必须创建两个套接字

sockSer=socket(AF_INET,SOCK_STREAM,0);                      //初始化套接字

SOCKADDR_IN addrSer,addrCli;        //注意服务器端要创建两个套接字地址

addrSer.sin_family=AF_INET;

addrSer.sin_port=htons(5566);

addrSer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

 

bind(sockSer,(SOCKADDR*)&addrSer,sizeof(SOCKADDR));        //绑定套接字

listen(sockSer,5);                               //监听

int len=sizeof(SOCKADDR);

cout<<"服务器等待客户端的连接……"<<endl;

 

sockConn=accept(sockSer,(SOCKADDR*)&addrCli,&len);      //接受连接,注意返回值

if(sockConn==INVALID_SOCKET){

        cout<<"服务器接受客户端连接失败!"<<endl;

        return 0;}

else  cout<<"服务器接受客户端连接成功!"<<endl;

 

char sendbuf[256],recvbuf[256];

while(1){

       if(recv(sockConn,recvbuf,256,0)>0)           //如果recv返回值大于0则输出消息

               cout<<"客户端说:>"<<recvbuf<<endl;

        else  {cout<<"客户端已断开连接"<<endl;

               break;}

        cout<<"服务器说:>";

        cin>>sendbuf;                      //将用户输入保存到sendbuf中

        if(strcmp(sendbuf,"bye")==0){ break;  }     //输入bye则退出while循环

        send(sockConn,sendbuf,strlen(sendbuf)+1,0);     //发送消息

}

closesocket(sockSer);

WSACleanup();

return 0;  }

说明:

1)在bind()函数中要将sockaddr_in的地址强制转换成sockaddr的地址。sockaddr是一种通用的套接字地址。而sockaddr_in是Internet环境下套接字的地址形式,可以分别设置IP地址和端口号。对于(SOCKADDR*)&addrSer,也可写成(LPSOCKADDR)&addrSer,因为LPSOCKADDR表示sockaddr的指针类型。

2)sizeof(sockaddr)也可写成sizeof(addrSer),因为sizeof是操作符,既可以用变量类型作为操作数,也可以用变量作为操作数,用来求变量占据的内存空间大小。除此之外,还可以用函数作为参数,如sizeof(fun()),其结果是函数返回类型的大小。

3)用closesocket()关闭套接字将导致TCP连接断开,而TCP断开连接采用四次握手机制,也会向对方发送数据包(但这种数据包只有包头,内容为空),这时会触发对方recv函数的执行。为此,可判断recv函数的返回值是否为0,如果是0,则表明是closesocket()函数断开连接时发送的数据包,否则是send()函数发送的数据包。

4)closesocket()既能用来关闭监听套接字closesocket(sockSer),也能用来关闭通信套接字closesocket(sockConn),当通信套接字关闭后,监听套接字仍然能继续运行,反之则不然。

5)WinSock中的函数都是全局函数,也就是说这些函数不属于任何类。因此可在这些函数前加“::”,例如::bind()、::listen()、::accept()等,因为“::”运算符左边是类名,如果不属于任何类,则“::”左边为空。

提示:在VC6中选中代码,再按Alt+F8键可自动实现代码缩进。

2.2.2 客户端程序的制作

新建工程,选择“Win32 Console Application”,输入工程名(如TCPClient),在下一步中选择“一个简单的程序”。在FileView工作区中,双击Source Files下面的“工程名.cpp”的源文件,输入如下代码,编译,运行。

#include<iostream.h>

#include<winsock2.h>

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

int main(){     

       WSADATA wsaData;    

       if(WSAStartup(MAKEWORD(2,2), &wsaData)) {

              cout<<"Winsock不能被初始化!";       WSACleanup();

              return 0;  }    

       SOCKET sockCli;                       //创建套接字sockCli

       sockCli=socket(AF_INET,SOCK_STREAM,0);

      

       SOCKADDR_IN addrSer;           //客户端只要创建一个套接字地址

       addrSer.sin_family=AF_INET;

       addrSer.sin_port=htons(5566);

       addrSer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");

      

       int res=connect(sockCli,(SOCKADDR*)&addrSer, sizeof(SOCKADDR));

       if(res){           cout<<"客户端连接服务器失败"<<endl;

              return -1; }

       else{       cout<<"客户端连接服务器成功"<<endl;     }

       char sendbuf[256], recvbuf[256];

       while(1){       

              cout<<"客户端说:>";

              cin>>sendbuf;

              if(strcmp(sendbuf,"bye")==0){     break;}

              send(sockCli,sendbuf,strlen(sendbuf)+1,0);

              if(recv(sockCli,recvbuf,256,0)>0)

                     cout<<"服务器说:>"<<recvbuf<<endl;

              else {cout<<"服务器已关闭连接"<<endl;

                     break;}

       }    

       closesocket(sockCli);

       WSACleanup();

       return 0;  }

该TCP通信程序的要点如下:

① inet_addr()函数的参数是一个字符串形式的IP地址,但也可设置为INADDR_ANY,表示该套接字的IP地址由系统自动指定。htons()函数的参数是一个数值型的端口号,如果将该参数定义为0,则系统将自动为套接字分配一个端口号。

② 注意accept()函数的第一个参数是监听套接字,而返回值是与客户端建立连接的通信套接字。

③ 在客户端和服务器程序中,send()和recv()函数的第1个参数的值总是通信套接字。send()的第3个参数是要发送的数据字节数,一般用strlen(buf)+1获得,而recv()的第3个参数是接收缓冲区buf的长度,必须用sizeof(buf)获得。

提示:无论是客户端还是服务器端,bind()函数绑定的地址永远都是绑定本地地址,connect()函数中的地址则永远是远程地址。

2.2.3 Winsock的错误处理

WinSock函数在执行结束时都会返回一个值,如果函数执行成功,需要返回函数执行结果的函数通常返回值就是执行结果(如socket()函数返回一个SOCKET类型),而对于无执行结果的函数,执行成功一般会返回0(例如connect()函数)。如果函数执行不成功,则绝大多数函数都会返回SOCKET_ERROR,虽然通过该返回值可以知道函数调用不成功,但无法判断函数不能成功执行的原因。

为此,WinSock提供了WSAGetLastError()函数解决该问题。该函数可获取上一次某个WinSock函数调用时的错误代码。函数原型如下:

int WSAGetLastError(void);

该函数的返回值就是上一次调用WinSock函数出错时所出错误对应的错误代码。

例如,当使用bind()函数为套接字绑定地址时,如果地址和端口号已被占用,则该函数会返回错误码10048;当使用Connect函数连接服务器时,如果服务器没有响应(可能还没启动),则该函数会返回错误码10061。下面是使用WSAGetLastError()函数举例:

if(bind(sockSer,(SOCKADDR*)&addrSer,sizeof(SOCKADDR)))

        cout<<"绑定套接字出错,错误代码为" <<WSAGetLastError()<<endl;

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个字节。

猜你喜欢

转载自blog.csdn.net/wuxia2118/article/details/88363615