实验四 单播通信实验

来源:互联网 发布:mac 便笺 保存位置 编辑:程序博客网 时间:2024/04/28 13:14

一、 实验目的

        掌握 TCP 服务器程序和客户程序的编程流程;
        熟悉面向连接的 C/S 程序使用的 winsock API。

二、实验设计

        (一)实验内容:
        1、编写一个TCP回显服务器,将收到的客户端信息发送给客户端,同时能在同客户端建立连接后显示客户端地址信息和当前连接数。
        2、编写一个TCP客户端程序能连接到编写的服务器,接收服务器信息。主函数使用int main(int argc,char** argv)形式传入服务器IP地址和端口。
        3、测试编写的程序,将测试数据、测试结果和结果分析写入实验报告。
        (二)背景知识
        流套接字编程模型
        流式套接字的服务器进程和客户端进程在通信前必须创建各自的套接字并建立连接,然后才能对相应的套接字进行“读”、“写”等操作,实现数据的传输。涉及的函数包括:
        1) 创建套接字函数socket
SOCKET socket(int af,int type,int protocol);
        //由于采用流套接字进行数据传输,因此type参数必须设置为SOCK_STREAM,protocol参数必须设置为IPPROTO_TCP。
        2) 绑定本地地址到所创建的套接字函数bind
int bind(SOCKET s,const struct sockaddr* name,int namelen);
        3) 监听网络连接请求函数listen
int listen(SOCKET s,int backlog);
        4) 连接请求函数connect
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen);
        5) 接受请求函数accept
SOCKET accept(SOCKET s,struct sockaddr* addr,int* addrlen);
        6) 发送数据函数send
int send(SOCKET s,const char* buf,int len,int flags);
        7) 接收数据函数recv
int recv(SOCKET s,char* buf,int len,int flags);
        8) 关闭套接字函数closesocket
int closesocket(SOCKET s);
        流套接字编程模型的时序和流程

        为便于理解流套接字模型下的编程过程,用时序图表述如下:、


图1 流套接字编程时序图

        以上时序图反映了采用流套接字编程模型时服务端、客户端的Winsock API调用顺序及两者之间的配合关系。服务器接受客户端的连接请求后,需要为该请求客户分配一个单独的套接字来完成与客户端的数据交换过程。简化的程序流程图表示如下:


图2 TCP服务器程序和客户程序的创建过程

        在以上过程示意图中,需要强调以下2点:
        1) 服务端通过socket()函数建立的套接字sListen与在accept函数返回后得到的新套接字sClient是两个不同的套接字,区别在于:前者用于服务端监听连接,在服务端主进程中只创建一次,并且绑定一个固定的端口号,以便客户端根据该端口号进行连接;后者用于与具体某个发起连接请求的客户端进行数据的交换。它们之间的联系在于:套接字sListen在监听到连接后,会将该连接的相关请求放到一个缓冲区中,套接字sClient在处理完上一个连接后,会查询该缓冲区的状态,并据此决定是否需要开始一次新的通信过程。请对照图3-3认真领会服务端两类套接字的作用。


图3 流套接字编程模型对应工作原理示意图

        2) 客户在建立连接后,可与服务端负责数据通信的套接字sClient进行多次数据交换,只有在所有数据交换完成后,由任一方执行closesocket(),双方才会关闭该次连接,并释放对应的套接字。
        (三)实验设计
        1、服务器端设计:每连接一个客户端就开一个线程,每个线程里循环接收和回显客户端的数据,直至客户端发送quit命令后退出。
        1) WinSock编程框架
        2) 创建用于监听的流式套接字sServer
        3) 设置监听套接字为非阻塞模式
        4) 设置服务器套接字地址
        5) 绑定sServer到设置的本地地址
        6) 监听sServer
        7) 循环等待接受客户端请求
        8) 建立连接,accept函数返回,得到新的套接字sClient
        9) 建立连接的客户端数目count+1
        10)为新连接上的客户(sClient)创建收发数据的线程
        11)关闭套接字sServer
        2、线程设计:循环接收和回显客户端的数据,直至客户端发送quit命令后退出。
        1) 清空接收数据的缓冲区
        2) 接受来自客户端的数据
        3) 获取系统时间,打印输出时间及数据等信息
        4) 如果客户端发送quit字符串,当前连接数count-1;否则向客户端发送回显字符串
        5) 关闭套接字sClient
        3、客户端设计:等待连接成功后,循环向服务器发送字符串,并显示反馈信息
        1) WinSock编程框架
        2) 创建用于与服务器进行通信的流式套接字sHost
        3) 设置套接字为非阻塞模式
        4) 设置服务器地址(可通过命令行参数输入)
        5) 循环等待连接到服务器,直至连接成功
        6) 循环等待向服务器发送数据
        7)  循环等待清空接收数据的缓冲区,接收服务器回传的数据,如果收到quit,则退出
        8)  关闭套接字sHost

三、实验过程

        (一)实验步骤
        1、编写TCP回显服务器。
        2、编写TCP客户端。
        3、在客户端的命令行输入参数。
        4、运行、调试。
        (二)实验过程
        1、设置服务器地址和端口号。


图4 输入命令参数

        2、运行服务器


图5 运行服务器

        3、运行客户端


图6 运行客户端

        客户端与服务器已经保持连接。

        4、发送数据


图7 发送数据
        5、运行多个客户端


图8 运行多个客户端

四、讨论与分析

        1、 accept()函数,connect( )函数会阻塞吗?如果阻塞,说明在什么情况下阻塞。 请给出在VC环境下的验证方法。
        accept()函数在请求连接的队列为空时会阻塞,直到有新的用户连接请求并响应,而当请求连接的队列不为空时,将获取请求队列中的请求并做相应处理。
        测试方法:在accept函数调用前打印字符串“before accept function”,并记录当前系统时间,在accept函数调用之后打印字符串“after accept function”和当前系统时间与前一时间的时间差,然后开启服务器,不启动任何客户端程序,观察控制台的输出情况,若只打印了“before accept function”则说明程序阻塞。然后重新开启服务器,并同时开启多个(限定数量内)客户端程序向服务器发起连接请求,观察控制台的输出,若同时打印并“before accept function”和“after accept function”字符串,并且打印的时间间隔很小,则说明没有阻塞。
        connect()函数不会发生阻塞,当客户端发起连接请求,而没有得到服务器回应,如服务器未开启或超出最大连接数,客户端的请求会自动返回连接失败。
测试方法:在connect函数调用前打印字符串“before connect function”,在其调用后打印“after connect function”,同样记录并打印时间差,在不开启服务器程序时,直接打开客户端程序发起连接请求,观察控制台的输出,若打印了“before connect function”和“after connect function”,且打印的时间差很短,则说明connect函数不阻塞。
        2、 connect()函数调用触发什么过程?
        connect()函数的调用将触发三次握手过程。
        第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认。
        第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态。
        第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
        3、 你在服务端和客户端分别使用了哪些Winsock API函数,起什么作用?
        服务器端:

socket()函数创建了一个套接字

htons()函数将参数从主机字节顺序转化到TCP/IP网络字节顺序

bind()函数将创建的套接字绑定到本地地址

listen()函数让套接字进入监听模式并制定最大连接数

accept()函数接收连接请求并创建新的套接字

recv()函数接收客户端发送的数据

send()函数向客户端发送数据

inet_ntoa()函数将32位的二进制数转化为了字符串

closesoke()t函数关闭制定的套接字

        客户端:

① socket函数创建一个无名的TCP类型的套接字

② htons函数将参数从主机字节顺序转化到TCP/IP网络字节顺序,

③ connect函数向制定地址的服务器端发送连接请求

④ send函数向客户端发送数据

⑤ recv函数接受客户端发送的数据

⑥ inet_ntoa函数将32位的二进制数转化为了字符串

⑦ inet_addr函数将字符串类型的IP地址转化为32为二进制数

⑧ closesoket函数关闭制定的套接字。

        4、 服务器是否实现了处理多个客户端的功能?
        实现了。由实验结果截图可看出。
        5、 服务器端的连接数量是怎么实现的?
        通过检测当前请求连接的情况,每当有新的请求便增加一个线程,连接数量也增加1。
        6、 在哪里给线程传递了参数?
        在服务器端主函数中,当客户端连接成功,则创建线程,从CreateThread()的第四个参数传递参数
        CreateThread(NULL, NULL, AnswerThread, (LPVOID)sClient, 0, NULL);
        7、 程序中的套接字是什么模式?
        客户端用于与服务器进行通信的套接字sHost为非阻塞模式;
        服务器端用于监听的套接字sServer为非阻塞模式;
        服务器端用于的套接字sClient为阻塞模式。

五、总结

        流式套接字的服务器进程和客户端进程在通信前必须创建各自的套接字并建立连接,然后才能对相应的套接字进行“读”、“写”等操作,实现数据的传输。对于函数socket()、bind()、listen()、connect()、accept()、send()、recv()、closesocket()等函数更加熟悉。服务端通过socket()函数建立的套接字sListen与在accept函数返回后得到的新套接字sClient是两个不同的套接字。区别在于:前者用于服务端监听连接,在服务端主进程中只创建一次,并且绑定一个固定的端口号,以便客户端根据该端口号进行连接;后者用于与具体某个发起连接请求的客户端进行数据的交换。它们之间的联系在于:套接字sListen在监听到连接后,会将该连接的相关请求放到一个缓冲区中,套接字sClient在处理完上一个连接后,会查询该缓冲区的状态,并据此决定是否需要开始一次新的通信过程。对于服务器的地址可以采用main函数自带的参数进行传参,另一种方法是可以在控制台对其进行赋值。

六、附录:关键代码

        1、服务器端:

#include "stdafx.h"#include <WINSOCK2.H>#include <windows.h>   #include <iostream>#pragma comment(lib,"WS2_32.lib")   #define BUF_SIZE    64      // 缓冲区大小sockaddr_in addrClient;// 客户端地址int count = 0;DWORD  WINAPI  AnswerThread(LPVOID  lparam){char   buf[BUF_SIZE];// 用于接受客户端数据的缓冲区   int     retVal;// 调用各种Socket函数的返回值   SOCKET  sClient = (SOCKET)(LPVOID)lparam;// 循环接收客户端的数据,直接客户端发送quit命令后退出。  while (true){ZeroMemory(buf, BUF_SIZE);// 清空接收数据的缓冲区retVal = recv(sClient, buf, BUFSIZ, 0);// 接收来自客户端的数据,因为是非阻塞模式,所以即使没有数据也会继续if (SOCKET_ERROR == retVal){int err = WSAGetLastError();// 获取错误编码if (err == WSAEWOULDBLOCK)// 接收数据缓冲区暂无数据{Sleep(100);continue;}else if (err == WSAETIMEDOUT || err == WSAENETDOWN){printf("recv failed !\n");closesocket(sClient);WSACleanup();return -1;}}// 获取当前系统时间SYSTEMTIME st;GetLocalTime(&st);char sDateTime[30];sprintf(sDateTime, "%4d-%2d-%2d %2d:%2d:%2d", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);// 打印输出的信息printf("%s, 接收自客户端 [%s:%d] ,信息为:%s\n", sDateTime, inet_ntoa(addrClient.sin_addr), addrClient.sin_port, buf);// 如果客户端发送quit字符串,则服务器退出if (strcmp(buf, "quit") == 0){retVal = send(sClient, "quit", strlen("quit"), 0);printf("客户端 [%s:%d]已退出\n", inet_ntoa(addrClient.sin_addr), addrClient.sin_port);count--;printf("当前连接数:%d\n", count);break;}else// 否则向客户端发送回显字符串{char    msg[BUF_SIZE];sprintf(msg, "信息 : %s 已经接收", buf);while (true){// 向服务器发送数据retVal = send(sClient, msg, strlen(msg), 0);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK)// 无法立即完成非阻塞套接字上的操作{Sleep(500);continue;}else{printf("send failed !\n");closesocket(sClient);WSACleanup();return -1;}}break;}}}// 关闭套接字closesocket(sClient);return 0;}int _tmain(int argc, _TCHAR* argv[]){WSADATA wsd;// WSADATA变量,用于初始化Windows Socket   SOCKET  sServer;// 服务器套接字,用于监听客户端请求SOCKET  sClient;// 客户端套接字,用于实现与客户端的通信   int     retVal;// 调用各种Socket函数的返回值   char    buf[1024];   // 用于接受客户端数据的缓冲区// 初始化套接字动态库   if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0){printf("WSAStartup failed !\n");return 1;}// 创建用于监听的套接字   sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);if (INVALID_SOCKET == sServer){printf("socket failed !\n");WSACleanup();return -1;}// 设置套接字为非阻塞模式int iMode = 1;retVal = ioctlsocket(sServer, FIONBIO, (u_long FAR*) &iMode);if (retVal == SOCKET_ERROR){printf("ioctlsocket failed !\n");WSACleanup();return -1;}// 设置服务器套接字地址   SOCKADDR_IN addrServ;addrServ.sin_family = AF_INET;addrServ.sin_port = htons(9990);// 监听端口为9990addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);// 绑定套接字sServer到本地地址,端口9990  retVal = bind(sServer, (const struct sockaddr*)&addrServ, sizeof(SOCKADDR_IN));if (SOCKET_ERROR == retVal){printf("bind failed !\n");closesocket(sServer);WSACleanup();return -1;}// 监听套接字   retVal = listen(sServer, SOMAXCONN);if (SOCKET_ERROR == retVal){printf("listen failed !\n");closesocket(sServer);WSACleanup();return -1;}// 接受客户请求   printf("TCP服务器已启动...\n");int addrClientlen = sizeof(addrClient);// 循环等待while (true){sClient = accept(sServer, (sockaddr FAR*)&addrClient, &addrClientlen);if (INVALID_SOCKET == sClient){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK)// 无法立即完成非阻塞套接字上的操作{Sleep(100);continue;}}count++;printf(" 客户端 [%s:%d]已经连接\n",  inet_ntoa(addrClient.sin_addr), addrClient.sin_port);printf("当前连接数:%d\n", count);CreateThread(NULL, NULL, AnswerThread, (LPVOID)sClient, 0, NULL);}// 释放套接字   closesocket(sServer);WSACleanup();// 暂停,按任意键退出system("pause");return 0;}
        2、客户端:

#include "stdafx.h"#include "stdafx.h"#include "stdafx.h"#include <Winsock2.H>   #include <string>#include <iostream>using namespace std;#pragma comment(lib,"WS2_32.lib")   #define BUF_SIZE    64          // 缓冲区大小 int _tmain(int argc, _TCHAR* argv[]){WSADATA     wsd;// 用于初始化Windows Socket   SOCKET      sHost;// 与服务器进行通信的套接字   SOCKADDR_IN servAddr;    // 服务器地址   char        buf[BUF_SIZE];// 用于接受数据缓冲区   int         retVal;// 调用各种Socket函数的返回值   // 初始化Windows Socketif (WSAStartup(MAKEWORD(2, 2), &wsd) != 0){printf("WSAStartup failed !\n");return 1;}// 创建套接字   sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);if (INVALID_SOCKET == sHost){printf("socket failed !\n");WSACleanup();return -1;}// 设置套接字为非阻塞模式int iMode = 1;retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) &iMode);if (retVal == SOCKET_ERROR){printf("ioctlsocket failed !\n");WSACleanup();return -1;}// 设置服务器地址   servAddr.sin_family = AF_INET;servAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);// 用户需要根据实际情况修改servAddr.sin_port = htons(atoi(argv[2]));    // 在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中int sServerAddlen = sizeof(servAddr);// 计算地址的长度       // 循环等待while (true){// 连接服务器   Sleep(200);retVal = connect(sHost, (SOCKADDR*)&servAddr, sizeof(servAddr));Sleep(200);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK || err == WSAEINVAL)// 无法立即完成非阻塞套接字上的操作{//Sleep(500);continue;}else if (err == WSAEISCONN)// 已建立连接{break;}else{continue;}}}// 循环向服务器发送字符串,并显示反馈信息。// 发送quit将使服务器程序退出,同时客户端程序自身也将退出while (true){// 向服务器发送数据   printf("请输入要发送的数据:\n ");// 接收输入的数据std::string str;cin >> str;// 将用户输入的数据复制到buf中ZeroMemory(buf, BUF_SIZE);strcpy(buf, str.c_str());// 循环等待while (true){// 向服务器发送数据retVal = send(sHost, buf, strlen(buf), 0);if (SOCKET_ERROR == retVal){int err = WSAGetLastError();if (err == WSAEWOULDBLOCK)// 无法立即完成非阻塞套接字上的操作{Sleep(500);continue;}else{printf("send failed !\n");closesocket(sHost);WSACleanup();return -1;}}break;}while (true){ZeroMemory(buf, BUF_SIZE);// 清空接收数据的缓冲区retVal = recv(sHost, buf, sizeof(buf), 0);   // 接收服务器回传的数据   if (SOCKET_ERROR == retVal){int err = WSAGetLastError();// 获取错误编码if (err == WSAEWOULDBLOCK)// 接收数据缓冲区暂无数据{Sleep(100);//printf("waiting back msg !\n");continue;}else if (err == WSAETIMEDOUT || err == WSAENETDOWN){printf("recv failed !\n");closesocket(sHost);WSACleanup();return -1;}break;}break;}printf("从服务器端返回信息: %s\n", buf);// 如果收到quit,则退出if (strcmp(buf, "quit") == 0){printf("quit!\n");break;}}// 释放资源   closesocket(sHost);WSACleanup();// 暂停,按任意键继续system("pause");return 0;}

         注:本博客源代码下载地址:http://download.csdn.net/detail/dmxexcalibur/9904521