前言:2017年6月份计算机网络的课设任务,在同学的帮助和自学下基本搞懂了,基于UDP协议的基本聊天的实现方法。实现起来很简单,原理也很简单,主要是由于老师必须要求使用C语言来写,所以特别麻烦,而且C语言的socket编程我基本没有接触过,顶多对java网络编程有一点涉猎。下面我将自己所学的知识做了一个总结,希望可以对想要去接触socket(网络)编程的同学有一个帮助,当然想要学好网络编程肯定是离不开几本书的支撑的,这篇文章主要通过一个机器人聊天的案例帮大家入下门。
注意:想要成功运行的前提条件是你别忘记,把我的代码的ip地址改一下(查看自己ip地址用ipconfig命令,详细查看《常见网络命令之traceroute命令一起其他常用命令 - 不忘初心 - 博客频道 - CSDN.NET 》),以及问答文本文档创建一下,一定要按照我的格式。任何一个问题都会导致程序无法运行 项目地址:基于UDP协议的简易聊天机器人 - 下载频道 - CSDN.NET一,先上效果图
1,半智能聊天+服务器可自定义内容(输入匹配 的内容自动回复,输入不匹配的内容服务器可以自定义回复):
2,服务器自动回复没有自定义回复 附加(自动回复问题内容文档):二,目的和意义
(1)意义:
将理论运用于实践,更深入地掌握计算机网络的核心内容。用具体的实践成果,体现对理论知识的掌握程度,提高计算机网络的实践能力,加深对计算机网络理论知识的理解,特别是网络通信这一块的理解。
(2)目的:
- 培养理论联系实际的设计思想,训练综合运用所学的基础理论知识,结合生产实际分析和解决网络应用中问题的能力,从而使基础理论知识得到巩固和加深。
- 学习掌握网络应用工程的一般设计过程和方法。
- 通过 基于udp协议的socket编程加深对与udp协议的理解以及与tcp协议的区分、
- 掌握c语言socket编程的基本方法,简单网络编程的编程思想。
二,要求和涉及的知识点
三,具体实现过程
(1)基本的知识点:
1,UDP基本介绍以及聊天程序选用UDP的好处
在我们学习UDP的时候就知道,UDP不需要建立连接,而且没有数据确认和数据重传的机制,所以实时性较高而且花费开销特别小。在聊天时即使丢失一些数据也不会影响信息的交流,我们可以通过上下文语义知道对方所要表达的意思,或者根据对方的信息重新发送我们要说的话;对于TCP来说,在通讯前要经过三次握手协议建立连接,而建立连接的过程往往是比较耗费时间的,连接建立之后,我们在聊天时候可能过很长时间才说一句话,那么连接是保持呢还是先断开,等对方说话时再建立连接呢?所以在聊天中TCP面向连接、数据确认与重传的机制将会影响聊天的效率。所以一般聊天类的程序一般都会选择UDP而不是TCP。2,客户服务器模式
3,数据报式套接字(SOCK_DGRAM) 提供了一个无连接服务(UDP)。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。 下图为无连接协议(UDP)的套接字调用: 4,重要函数 recvfrom(int sockfd,void* buff,size nbytes,int flags,struct sockaddr* from,socklen_t *addrlen); 参数:sockfd : 套接字描述符, buff : 用于存放数据的缓冲区,nbytes : 缓冲区大小,flags : 暂时总设置为0,from : 用于存放UDP对端的套接字协议地址(输出参数)addrlen : UDP对端的套接字协议地址字节大小(输出参数) 注意:最后两个参数from和addrlen可以得知该UDP数据是谁发送过来的。 如果设置from和addrlen为NULL,表示我们忽略对端信息。 返回值: 成功返回读取到的字节数,出错返回-1。返回值0是被允许的,不同于TCP中read返回0表示对端已经关闭。 sendto(int sockfd,const void* buff,size nbytes,int flags,const struct sockaddr* to,socklen_t *addrlen); 参数:sockfd : 套接字描述符;buff : 要发送的数据内存;nbytes : 数据大小;flags : 一般设置为0;to : 指向接收者的套接字地址结构(输入参数);addrlen : 上述套接字地址结构to的字节大小(输入参数) 注意:最后两个参数to和addrlen告知该数据要发给谁。 返回值: 成功返回写入的字节数,出错返回-1 写入字节长度为0是被允许的,在UDP下,会形成一个只包含20字节的IP首部和一个8字节的UDP首部,没有数据的IP数据报。(2)代码清单(含详细注解):
客户端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include#include #pragma comment(lib, "ws2_32.lib") void main(){ //windows操作系统下的初始化工作,加载套接字库 WORD wVersionRequested; WSADATA wsaData; int err; char addr[20] = { 0 }; wVersionRequested = MAKEWORD(1, 1);//第一个参数为低位字节;第二个参数为高位字节 err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { return; } if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) { WSACleanup(); return; } //创建套接字 SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, 0); //基于UDP的客户端来说,它不需要去绑定,但是要设置信息将要发送到对方机器的地址信息,也就是服务器端的地址信息 SOCKADDR_IN addrSrv; //定义一个地址结构体的变量, printf("************如遇到发送消息,机器人不回复的情况,请稍等片刻,机器人正在处理你的消息************\n"); printf("************ 输入 q 即可结束对话 ************\n"); //获取服务器地址 addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.116"); addrSrv.sin_family = AF_INET;//地址族,,代表TCP/IP协议族 addrSrv.sin_port = htons(6000);//接收端口号 char recvBuf[100];//接收数据 char sendBuf[100];//发送数据 char tempBuf[200];//临时数据存储 int len = sizeof(SOCKADDR);//用于返回接收数据的地址结构的长度,必须先经过初始化 while (true) { printf("please input data:\n"); gets_s(sendBuf);//获取用户输入的数据 //UDP不需要建立连接直接通过sendto()函数发送数据 sendto(sockClient, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrSrv, len);//发送数据 recvfrom(sockClient, recvBuf, 100, 0, (SOCKADDR*)&addrSrv, &len);//接收数据 //判断是否结束对话 if ('q' == recvBuf[0] ) { sendto(sockClient, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrSrv, len); //一下语句实现倒计时功能 printf_s("对话结束,还有五秒即将关闭窗口\n"); int i; for (i = 5; i >= 0; i--) { printf("%d", i); Sleep(1000); printf("\b");//退格 即将当前光标位置回退一列 } printf(" "); printf("\n"); //程序退出 break; } //将接收到的数据格式化到tempBuf中 //inet_ntoa参数:一个网络上的IP地址;功能:将一个十进制网络字节序转换为点分十进制IP格式的字符串。 sprintf_s(tempBuf, "%s say:%s", inet_ntoa(addrSrv.sin_addr), recvBuf); printf("%s\n", tempBuf); } //关闭套接字 closesocket(sockClient); //终止对套接字库的使用 WSACleanup();}
服务器端:
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include#include #pragma comment(lib, "ws2_32.lib") void main(){ FILE *f; char szLine[MAX_PATH]; char buffer[MAX_PATH]; fopen_s(&f, "D:\\info.txt", "r"); if (f == NULL) { printf("无法打开文件\n"); return; } //windows操作系统下的初始化工作,加载套接字库 WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD(1, 1); err = WSAStartup(wVersionRequested, &wsaData); if (err != 0) { return; } if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) { WSACleanup(); return; } //创建套接字,因为是基于UDP的,所以用SOCK_DGRAM. SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0); //对于服务器端,接着应该进行绑定 SOCKADDR_IN addrSrv;//定义一个地址结构体的变量, addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.116"); //地址族,AF这个前缀表示地址族(address family) addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(6000);//端口号 bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); char recvBuf[100];//字符数组,用来接收信息 char sendBuf[100];//用来发送信息 char tempBuf[200];//用来存放中间数据 //定义一个地址结构体的变量,在通讯的时候, /*我们需要获取和我们通讯的这一方的地址信息, 这一获取是我们通过调用recvfrom来获得的,但是我们需要提供一个地址结构体的变量*/ SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); printf("服务器开启成功等待客户连接\n"); //while循环,保证通讯过程能够不断的循环下去 while (1) { //接收数据 recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrClient, &len); //判断是否结束对话,q表示结束 if ('q' == recvBuf[0]) { sendto(sockSrv, "q", strlen("q") + 1, 0, (SOCKADDR*)&addrClient, len); printf("对话结束!\n"); break; } //将数据格式化到tempBuf中 //addrClient.sin_addr表示对方的IP地址, //inet_ntoa将IP转换为点分十进制表示的形式,如172.0.0.1 sprintf_s(tempBuf, "%s say:%s", inet_ntoa(addrClient.sin_addr), recvBuf); //将数据打印输出 printf("%s\n", tempBuf); //把文件指针指向文件的开头 fseek(f, 0, SEEK_SET); //对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化为‘ ’或‘/0’;例:char a[100];memset(a, '/0', sizeof(a)); memset(szLine, 0, MAX_PATH); //szLine: 字符型指针,指向存储读入数据的缓冲区的地址。 //MAX_PATH: 从流中读入MAX_PATH - 1个字符 //f : 指向读取的流。 //取的数据保存在szLine指向的字符数组中 fgets(szLine, MAX_PATH, f); //printf("%s\n", szLine); while (szLine[0] != '#') { if (szLine[0] == 'Q') { char szTemp[MAX_PATH] = { 0 }; //复制字符串szLine + 2到缓冲区szTemp lstrcpyA(szTemp, szLine + 2); szTemp[lstrlenA(szTemp) - 1] = '\0'; //匹配成功找到答案 if (lstrcmpA(szTemp, recvBuf) == 0) { memset(szLine, 0, MAX_PATH); fgets(szLine, MAX_PATH, f); //向客户端发送udp数据报,即回答客户端的消息 //参数sockSrv为已建好连线的socket,如果利用UDP协议则不需经过连线操作。 //参数 szLine+2欲连线的数据内容,参数flags 一般设0, //szLine加2的原因是从读取的字符的第三个字符开始输出,因为前两个字符为A: sendto(sockSrv, szLine + 2, strlen(szLine) - 1, 0, (SOCKADDR*)&addrClient, len); printf("服务器对话框输出内容:\n"); printf("%s", szLine+2); break; } } memset(szLine, 0, MAX_PATH); fgets(szLine, MAX_PATH, f); //printf("%s\n", szLine); } if (szLine[0]== '#') { //注意把前三行代码注释后三行代码取消注释就是自动回复的 memset(buffer, 0, MAX_PATH); sprintf_s(buffer, "听不懂,"); sendto(sockSrv, buffer + 2, strlen(buffer) - 1, 0, (SOCKADDR*)&addrClient, len); //printf("Please input data:\n"); //gets_s(sendBuf);//从键盘输入数据 //sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrClient, len);//发送数据 } } //当循环结束的时候,关闭套接字 closesocket(sockSrv); //终止对套接字的使用 WSACleanup(); fclose(f);}
四,心得
在做这次课程设计的过程中遇到了很多问题,比如自己不是很了解c语言soket编程的一些具体函数,所以刚开始时不知道如何下手。花了几天时间把老师给的代码里的每一个函数好好查了一遍,并且对每个函数都有了一个认识。这也是以后工作的一个基础。做一个双方通信的程序很简单,但是如何做带一个自动回复的当时还是有点不理解,通过询问我们班一个网络编程学的还挺好的一位同学知道可以通过文本存放相关问题和答案然后在程序中读取搜索用户的输入与那个问题相匹配,然后再把相应的内容输出。、
我觉的这次课程设计的题目还挺好的,让我对网络的客户/服务器有了更深的理解以及对于UDP协议有了更加深入的认识和对于网络编程有了初步认识与了解及巩固了对文件读取的内容。 后面这段时间我会好好把java网络编程再深入学习一遍,相信有了基础我能很快上手做一些的小项目。 附加:通过上面的内容需要掌握还是很难,给大家推荐几篇博客: socket编程原理 - guisu,程序人生。 逆水行舟,不进则退。 - 博客频道 - CSDN.NET UDP套接字编程 - 蔷薇岛少年的博客 - 博客频道 - CSDN.NET 套接字socket 的地址族和类型、工作原理、创建过程 - 推酷 memset函数使用详解 - 追逐. - 博客园 SOCKADDR_IN_百度百科