自已建网站,网站建设的步骤及方法,什么叫微网站,揭东建设局网站目录
V1_Echo_Server
V2_Echo_Server多进程版本
V3_Echo_Server多线程版本
V3-1_多线程远程命令执行
V4_Echo_Server线程池版本 V1_Echo_Server TcpServer的上层调用如下#xff0c;和UdpServer几乎一样#xff1a; 而在InitServer中#xff0c;大部分也和UDP那里一样和UdpServer几乎一样 而在InitServer中大部分也和UDP那里一样不同的是使用socket时第二个参数是SOCK_STREAM。 除了创建socket和bind外还有第三步因为tcp是面向连接的tcp需要未来不断地能够做到获取连接需要将server套接字设为listen状态以便随时等待被获取连接 其中backlog一般设为较小的数字比如4、8等。 此时server处于listen状态等待别人随时来连接自己listen就比如饭馆老板一天随时等待客人来吃饭。然后我们可以添加一个_isrunning的成员变量以表明服务器的运行状态初始化为false。
在server处于listen状态后因为tcp是需要连接的需要使用accept函数来获取连接 其中第一个参数是server的套接字后两个参数是用来得到是谁来连接server。关键在于accept的返回值 我们看到accept的返回值竟然是一个文件描述符这就让我们有点蒙圈了。因为在之前写udp代码时只有一个文件描述符那么此时我们难免有这样两个疑问
return fd是什么return fd 和 _sockfd的关系
我们来将一个小故事比如你和你的朋友去杭州西湖玩在那里附近有很多饭馆有一家叫西湖鱼庄这家店雇了张三在店外面拉客正好你在饭点碰到这家饭馆就被拉了进去吃饭张三带着你们进了饭店门口然后张三喊来客人了出来个人招呼客人然后李四就出来招呼你们了。然后张三又去店外面继续拉客过了不久张三又拉来了几个客人到了店里喊又来客人了出来个人招呼此时王五出来招呼这几个客人张三又跑出去继续拉客。在这个过程中张三不给客人提供服务只负责拉客。这个西湖鱼庄就是服务器一个个客户就是一个个连接而张三就是类成员_sockfd李四、王五就相当于accept的返回值return fd这个返回值来给连接提供服务_sockfd就是用来协助accept获取新连接。把这个只负责获取连接的_sockfd叫做listensockfd监听套接字。 把成员变量改为_listensockfd。
如果张三拉客失败也就是accept的返回值为0那会怎么样呢张三当然会继续拉客。 在提供服务时由于udp是面向数据报udp只能用recvfrom和sendto这样和网络强相关的接口而tcp是面向字节流。之前我们学过C/C的文件流以及管道的字节流这些都是“流”实际上它们都是一个东西Linux下一切皆文件所以网络、管道等都是文件所以只要符合相同的流的特性tcp这里的字节流的读取就相当于文件读取也就是可以使用read/write进行读取。当使用read进行读取时表明读取客户端结束文件中表示读到文件结尾这点有区别。 在客户端这里也是首先创建套接字然后不需要显式bind但是一定要有自己的IP和port所以需要隐式bindOS会用自己的IP和随机端口号去bind sockfd。客户端也不需要监听没人回来连接客户端。server在等连接所以客户端需要发起连接使用connect调用 那什么时候进行自动bind呢在创建连接成功时就会bindclient的代码如下
int main(int argc, char* argv[])
{if(argc ! 3){std::cerr Usage: argv[0] server_ip server_port std::endl;exit(0);}std::string server_ip argv[1];uint16_t server_port std::stoi(argv[2]);//1.创建socketint sockfd ::socket(AF_INET, SOCK_STREAM, 0);if(sockfd 0){std::cerr create socket error std::endl;exit(1);}//2.connectstruct sockaddr_in server;memset(server, 0 , sizeof(server));server.sin_family AF_INET;server.sin_port htons(server_port);::inet_pton(AF_INET, server_ip.c_str(), server.sin_addr.s_addr);int n ::connect(sockfd, (struct sockaddr*)server, sizeof(server));if(n 0){std::cerr connect socket error\n std::endl;exit(2);}while(true){std::string message;std::cout Enter# ;std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());char echo_buffer[1024];int n ::read(sockfd, echo_buffer, sizeof(echo_buffer)-1);if(n 0){echo_buffer[n] 0;std::cout echo_buffer std::endl;}else{break;}}::close(sockfd);return 0;
}
我们编译运行这份代码当启动第一个客户端时发现可以正常echo 然后我们再启动第二个客户端发现服务器没有和第二个客户端建立连接也没有echo 只有把第一个客户端退出后服务器才能和第二个客户端建立连接服务器才能echo第二个客户端 因此我们发现这版客户端代码没有并发处理能力一次只能处理一个客户端这时因为主线程一直在Service内部在运行 所以为了解决以上服务器端不能并发处理的问题
V2_Echo_Server多进程版本
因此我们在处理Service时通过创建子进程来处理 父子进程都要有独立的文件描述符表而子进程的文件描述符表是从父进程那里拷贝来的注定了父子进程指向了同样的文件所以子进程肯定能看见创建的创建的sockfd代码是共享的数据以写时拷贝的方式各自私有一份也就是说父进程打开了多少个文件子进程可以看到并且能访问。父进程创建的listensockfd是3文件描述符子进程创建的sockfd是4号文件描述符子进程从父进程拷贝了文件描述符表所以和父进程指向同一个文件。因为子进程不关心3只关心4这里的建议是让子进程关闭listensockfd只保留sockfd。同时要求父进程关闭sockfd只保留listensockfd这里是要求如果父进程不关sockfd相当于4号文件描述符一直被占用如果再有客户端来连接服务器只能使用5号文件描述符来处理导致父进程的文件描述符一直在被打开而从来没有被关闭文件描述符的本质就是数组的下标数组下标肯定是有限个这就导致了文件描述符泄漏的问题。
所以我们期望的是父进程把自己该做的做完然后去回到accept继续等待被连接。而子进程去执行if(id 0)内部的代码这样就能做到服务器采用多进程的方式并发处理连接 可是父进程在waitpid时采用的是0阻塞式等待所以我们刚才想的理想过程不会发生子进程在处理任务期间父进程会阻塞等待这不是还是一次只能处理一个连接吗那怎么解决呢我们在学习信号的时候子进程在退出时会向父进程发送SIGCHID信号如果对SIGCHID进程ingore那父进程就不需要等子进程退出了只负责连接就行了这种方式是可行的也是最推荐的。 此外我们还可以这样做 在子进程中再创建子进程也就是孙子进程。if(fork() 0)exit(0)让子进程直接退了直接留下孙子进程。子进程返回了父进程就能等待成功然后返回了。当孙子进程处理完后就会变成孤儿进程被系统领养就不用再关心这个孙子进程了。但是这不是最好方案最好方案就是上面那种。
V3_Echo_Server多线程版本 创建新线程主线程会等待新线程这还是串行运行不能实现并发访问。为此我们想到之前学过线程分离不再让主线程等待新线程而是让新线程分离 那用于执行任务的文件描述符sockfd怎么交给新线程呢我们知道新线程和主线程是共享同一张文件描述符表的这里绝对不能让主线程和新线程关闭自己不用的套接字fd也不需要了。我们把Execute函数设置为了static属性不能访问类内方法不能访问类内的Service方法为此我们创建一个内部类ThreadData V3-1_多线程远程命令执行
由远程发过来命令行字符串server对命令行字符串进行执行把执行结果返回给远程。建立Command.hpp头文件 我们进行网络的读取不仅仅可以使用read/write接口还可以使用recv/send这一对接口这两个接口不能用来读取udp只能读取tcp是面向字节流的读取。 recv/send的flags默认设为0。Command类的设计如下HandlerCommand函数用于处理客户端传来的字符串通过Excute函数来把传入的字符串做解释 那在Excute拿到待解释的命令行字符串后怎么解释这个字符串呢我们可以使用popen函数调用 popen内部会建立一个管道文件然后创建子进程执行对应的command命令内部来帮我们做命令行解析解析后的内容放到管道文件中返回FILE*让我们以文件的方式读取管道。换句话说未来只需要命令字符串传给popen就可以了像读文件一样把结果读出来。第二个参数type是r/w/a。通过pclose把对应的管道文件关闭。
class Command
{
public:Command(){_safe_command.insert(ls);_safe_command.insert(touch);_safe_command.insert(pwd);_safe_command.insert(whoami);_safe_command.insert(which); }~Command(){}bool CheckSafe(const std::string cmdstr){for(auto e : _safe_command){if(strncmp(e.c_str(), cmdstr.c_str(), e.size()) 0){return true;}}return false;}std::string Excute(const std::string cmdstr){if(!CheckSafe(cmdstr)) return unsafe;FILE* fp popen(cmdstr.c_str(), r);std::string result;if(fp){char line[1024];while(fgets(line, sizeof(line), fp)){result line;}return result;}return excute error;}void HandlerCommand(int sockfd, InetAddr addr){while (true){char commandbuff[1024];ssize_t n ::recv(sockfd, commandbuff, sizeof(commandbuff) - 1, 0); // TODOif (n 0){commandbuff[n] 0;LOG(INFO, get command from client %s, command : %s\n, addr.AddrStr(), commandbuff); std::string result Excute(commandbuff);::send(sockfd, result.c_str(), result.size(),0);}else if (n 0){LOG(INFO, client %s quit\n, addr.AddrStr().c_str());break;}else{LOG(ERROR, read error: %s quit\n, addr.AddrStr().c_str());}}}
private:std::setstd::string _safe_command;
};
运行结果如下 实际上我们打开Xshell实际上是打开了一个客户端在Xshell上输入命令其实是将命令发送到远端去请求服务器上的一个长启动的服务把命令行字符串交给它由它执行并推送给客户端执行结果。所以我们所谓的命令执行就是推送到远端。
V4_Echo_Server线程池版本
实际上这种Service长服务不太适合用线程池因为线程池中的线程是有上限的每个线程一直被占用。这次的线程池版本只是一个示例未来还是要使用V2版本的多线程。创建任务类型task_t这是线程池中任务的类型
using func_t std::functionvoid();
然后构建任务放到线程池中去处理 总结一下tcp就是通过listensocket套接字去获取连接把新连接和客户端地址交给别人去处理可以多并发地去处理。