一. 什么是协议
下面是百度百科对计算机协议意思的解释:
协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。
通俗的讲协议是一种约定,约定所有参与方的行为。没有协议就会混乱,说不清道不明或不在同一频道,即协议是一种背景知识和行为规范。
二. 为什么要有协议
为了使数据在网络上能够从源到达目的地,网络通信的参与方必须遵循相同的规则,这套规则就称为协议(protocol),它最终体现为在网络上传输的数据包的格式。
三. 利用套接字实现一个简单的网络计算器
1. 代码实现
通信的形式多种多样,只要涉及通信就涉及到协议。要实现网络版计算器,我们把需要计算的数据和操作符保存到一个类对象(Request)中,然后客户端把这个类对象通过网络发给服务端;服务端拿到这个类对象后完成计算并把计算结果存储到另外定义的一个类对象(Respond)中,并返回给客户端,这样一次通信就完成了。
protocol.h
需要计算的数据保存到class Request类对象中,计算结果保存到class Respond类对象中,这两种结构要保证客户端和服务端同时都能看到,所以我们把它俩的声明放到一个头文件中,这个头文件里的内容就相当于客户端、服务端的通信协议。
/* protocol.h */ #pragma once struct Request { // 两个整型操作数和一个操作符(+-*/%) int x; int y; char op; }; struct Respond { // 0 -> 正确运算 // 1 -> 除0错误 // 2 -> 模0错误 // 3 -> 非法运算符 int status = 0; int result = 0;// 运算结果 };
通信协议制定好了,现在我们要开始写客户端/服务端的代码了,应用层网络开发的话我们可以使用socket编程。
server.cpp
服务端通过TCP协议不断接收客户端发来的连接,然后派生一个子线程去专门处理该客户端的计算任务,子线程计算完成后把结果发还给客户端并继续等待客户端发来计算任务。
/* server.cpp */ #include "protocol.h" #include <unistd.h> #include <pthread.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <iostream> using namespace std; // 子线程执行函数 void* Routine(void* arg) { pthread_detach(pthread_self()); int linkSock = *(int*)arg; delete (int*)arg; // 针对处理某一个客户端发来的任务,直到客户端关闭或接收数据失败,这个线程才结束 while(1) { Request rq; ssize_t size = recv(linkSock, &rq, sizeof(rq), 0); if(size > 0) { Respond rp; switch(rq.op) { case '+': rp.result = rq.x + rq.y; break; case '-': rp.result = rq.x - rq.y; break; case '*': rp.result = rq.x * rq.y; break; case '/': if(rq.y == 0) { rp.status = 1; } else { rp.result = rq.x / rq.y; } break; case '%': if(rq.y == 0) { rp.status = 2; } else { rp.result = rq.x % rq.y; } break; default: rp.status = 3; break; } send(linkSock, &rp, sizeof(rp), 0); } else if(size == 0) { cout<<"client close!"<<endl; break; } else { cerr<<"recv error"<<endl; break; } } close(linkSock); return nullptr; } int main(int argc, char* argv[]) { // 检查传入的命令行参数是否符合要求 if(argc != 2) { cerr<<"Usage:serverName serverPort"<<endl; exit(1); } // 解析传入的命令行参数 int port = atoi(argv[1]); // 创建套接字 int listenSock = socket(AF_INET, SOCK_STREAM, 0); if(listenSock < 0) { cerr<<"socket error"<<endl; exit(2); } // 绑定socket地址 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = htonl(INADDR_ANY); if(bind(listenSock, (struct sockaddr*)&local, sizeof(local)) < 0) { cerr<<"bind error"<<endl; exit(3); } // 设置监听套接字 if(listen(listenSock, 5) < 0) { cerr<<"listen error"<<endl; exit(4); } // 不断地建立连接,然后创建子线程去处理连接任务 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); while(1) { int linkSock = accept(listenSock, (struct sockaddr*)&peer, &len); if(linkSock < 0) { cout<<"accept fail, continue next"<<endl; continue; } else { cout<<"get a new link"<<endl; int* pLinkSock = new int(linkSock); pthread_t pid; pthread_create(&pid, nullptr, Routine, pLinkSock); } } return 0; }
client.cpp
启动客户端后,客户端首先要去和服务端建立连接关系,连接完成后从键盘接收需要计算的数据,然后把这些数据通过网络发送给服务端的某一个线程让其处理这个计算任务,处理完成后把计算结果打印到屏幕上,一直循环这个过程。
/* client.cpp */ #include "protocol.h" #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <string.h> #include <iostream> using namespace std; int main(int argc, char* argv[]) { // 参数检查 if(argc != 3) { cerr<<"Usage:clientName serverIp serverPort"<<endl; exit(1); } // 参数解析 string serverIp = argv[1]; int serverPort = atoi(argv[2]); // 创建套接字 int linkSock = socket(AF_INET, SOCK_STREAM, 0); if(linkSock < 0) { cerr<<"socket error"<<endl; exit(2); } // 和服务端建立连接 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(serverPort); peer.sin_addr.s_addr = inet_addr(serverIp.c_str());// PS:注意用了inet_addr就不要在用htonl了,不然会转换出错 if(connect(linkSock, (struct sockaddr*)&peer, sizeof(peer)) < 0) { cerr<<"connect error"<<endl; exit(3); } // 死循环从外界获得算式并交给服务端线程去完成任务 while(1) { Request rq; cout<<"请输入左操作数(整数):"; cin>>rq.x; cout<<"请输入操作符(+-*/%):"; cin>>rq.op; cout<<"请输入右操作数(整数):"; cin>>rq.y; send(linkSock, &rq, sizeof(rq), 0); Respond rp; recv(linkSock, &rp, sizeof(rp), 0); cout<<"status:"<<rp.status<<' '<<"result="<<rp.result<<endl; } return 0; }
2. 结果测试
首先编译连接生成两个可执行程序:server和client
启动服务端,端口号我们设为8081
启动客户端,在本地环回(127.0.0.1)进行测试,发现连接成功
输入数据,计算的结果也没有问题
最后关闭客户端,此时服务端不会关闭,依然可以接收其他客户端发来的连接请求,而且可以并发的同时处理多个客户端任务。
四. 知识点补充
理解一下TCP/IP四层模型里的协议
下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是HTTP协议,这个协议是一些大佬已经写好的,在应用层我们只需要遵守规则去使用即可。
socket套接字处于网络协议的那一层?
socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议簇细节隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。而我们所说的socket编程指的是利用socket接口来实现自己的业务和协议。
综上所述:socke接口属于应用层与传输层中间的软件抽象层,而socket编程却是标准的应用层开发。