尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【Linux网络编程】--TCP_Socket

【Linux网络编程】--TCP_Socket 前面我们提到TCP是要提供可靠的通信那么其所要做的工作就要比UDP要多的多了下面我们先直接进行代码编写。首先不论是TCP还是UDP对于我们的服务端都不需要显示的bindIP只需要一个端口号即可。对于IP地址是使用随机绑定的方式。下面我们对服务端进行编写EchoTcpServer.hpp首先就是其服务端的基本结构和我们UDP的一样的就是一个初始化还有一个启动接口。下面我们完成对初始化接口的编写首先和UDP一样我们得先创建套接字socket不过和UDP不一样的是我们的TCP中的传输方式要选择字节流的方式。可以看到上面从建立socket到bind网络信息和UDP是基本一样的。但是要补充的是我们的TCP是要面向连接的那么我们的TCP服务器就要其处于一种listen监听状态。那么就要使用下面这个接口其作用是对一个socket进行监测让这个socket进行等待新链接到来。其第一个参数就是我们前面所创建的socket。然后其返回值成功那么就返回0失败就返回-1。就好比饭店的老板其是一直在店里等待顾客的。其第二个参数就是设置我们链接队列的长度。一般我们设置为16或者32。可以看到有个端口号为8080的tcp服务。如下是对服务端的初始化代码void InitServer() { // 创建soclet _sockfd socket(AF_INET, SOCK_STREAM, 0); if (_sockfd 0) { LOG(LogLevel::FATAL) creata socket error; exit(SOCKET_ERR); } LOG(LogLevel::DEBUG) create socket success_sockfd; // 填充网络信息 struct sockaddr_in local; memset(local, 0, sizeof(local)); local.sin_family AF_INET; local.sin_port htons(_port); local.sin_addr.s_addr INADDR_ANY; // 任意IP地址bind // bind int n bind(_sockfd, (struct sockaddr *)local, sizeof(local)); if (n 0) { LOG(LogLevel::FATAL) bind error; exit(BIND_ERR); } LOG(LogLevel::DEBUG)create bind success; // tcp是面向连接的所以TCP服务器要处于一种listen监听状态 int m listen(_sockfd, gbacklog); if (m 0) { LOG(LogLevel::FATAL) lisent error; exit(LISENT_ERR); } LOG(LogLevel::DEBUG)create lisent success; }下面我们来完成启动的接口对于TCP的启动我们不能直接和前面UDP的一样一上来就开始发送信息而是要先建立连接然后才可以进行通信。那么我们建立连接要使用下面这个接口首先其第一个参数就是前面我们在初始化的时候创建的socket。而后面两个参数就是帮我们获取客户端的socket的IP和port信息。然后其返回值如果其连接成功那么其会给我们返回一个文件描述符那么我们前面创建的sockfd和这个文件描述符有何区别?前面我们所创建是socket就相当于一个中介其是将我们给这个新连接的文件描述符进行链接的后续进行网络通信就和这个accept所返回的文件描述符进行通信那么我们也称为IOsocket。也就是服务socket。未来一个服务socket那么就代表一条连接。那么我们建立好了链接那么就要进行处理了。在我们的TCP中我们可以使用和文件读取一样的接口进行读取操作。当我们在进行读的时候当读取到的数据长度为0那么并不是出错而是说对方不想和我们进行通信了那么此时我们就可以退出循环那么就可以断开连接那么断开连接就是将我们的文件描述符关闭即可。那么如果读出错那么也不需要马上终止进程一样只需要先将其连接终止掉先。代码如下void serverIO(int soclfd, InetAddr address) { LOG(LogLevel::DEBUG) client info is: address.ToString(); while (true) { char inbuffer[1024] {0}; // 先读取数据 ssize_t nread(soclfd,inbuffer,sizeof(inbuffer)-1); if(n0) { inbuffer[n]0; LOG(LogLevel::INFO)address.ToString()sya#inbuffer; //写回去给客户端 std::string echo_string server echo#; echo_stringinbuffer; write(sockfd,echo_string.c_str(),echo_string.size()); } else if(n0) { LOG(LogLevel::INFO)client quit,address:address.ToString(); break; } else { LOG(LogLevel::ERROR)client read error ,address:address.ToString(); break; } } }如上就是我们服务端的基本代码下面我们完成对用户端进行编写。EchoTcpClient.cc在用户端首先我们也要进行socket的创建创建好socket后那么我们就要进行IP和port的绑定但是有个问题就是对于IP 和prot是否需要进行显示bind呢?为了防止在服务器端出现用户的端口号冲突的问题所以不需要我们去手动bindOS会对我们进行随机bind端口号。防止端口号冲突。那么我们用户端创建好socket后那么又该干嘛呢那么就要去和我们的服务端创建连接了。那么在用户端要使用如下接口进行连接那么当我们进行连接的时候其会为我们自动进行bind。//建立连接和bind InetAddr serveraddress(server_port,server_ip); int n connect(sockfd,(struct sockaddr*)serveraddress.GetNetAddress(),serveraddress.Len()); if(n0) { std::cerrconnect toserveraddress.ToString()failed!; exit(3); } std::cerrconnect toserveraddress.ToString()success; //进行通信 while(true) { std::string line; std::coutplease Enter#; std::getline(std::cin,line); write(sockfd,line.c_str(),line.size()); char inbuffer[1024]{0}; ssize_t nread(sockfd,inbuffer,sizeof(inbuffer)); if(n0) { inbuffer[n]0; std::coutinbufferstd::endl; } else if(n0) { std::coutread enf of fliestd::endl; break; } else{ std::cerrread error!std::endl; break; }那么还有一个问题就是当我们进行连接是一定就马上连接成功么实际上我们平时进行打游戏也会有感觉有时候会进行多次连接才会连接上所以我们进行TCP连接的时候是进行判断是否连接成功然后进行循环连接的情况。然后如果连接多次的情况那么我们就会进行终止进程的感觉。EchoTcpServer.hpp改写前面我们所写的服务端其是单线程的那么就是说当我们有用户再使用服务器的时候其他用户再去使用那么此时就不能马上得到消息反馈所以呢我们下面要对其进行改写改写成多进程的方式对于创建连接后对于消息的收发我们创建一个子进程来完成。但是我们的主进程也要对这个子进程来进行回收那么如果我们进行wait等待的话那么不也是要进行阻塞等待么不还是单进程的效果虽然我们可以使用waitpid来设置非阻塞等待但是这个效果也不是很好。我们要的多进程的版本是我们的主线程只去进行连接的工作而我们的子进程就去完成通信工作那么我们可以在我们的建立连接前进行一个信号捕捉的工作然后将子进程退出的时候返回的退出信号进行捕捉进行一个忽略处理那么子进程的退出我们主进程也进行处理了。而且也不需要进行wait等待子进程退出了。那么我们还有一个方法就是我们创建了子进程后我们的子进程又立马去创建一个子进程也就是创建孙子进程然后由孙子进程去处理通信工作我们的子进程立马退出那么我们的主进程在创建好子进程后立马就会等待到子进程的退出那么就可以马上返回到开始进行等待连接的工作了。然后对于孙子进程其就变成孤儿进程那么后续会被我们的系统进行回收处理。然后还要补充的是对于我们的主进程我们再创建了子进程后那么我们就不需要连接后返回的那个文件描述符了所以为了资源的利用效率我们要将其关闭对于子进程也是如此我们子进程不关心连接工作只需要进行通信即可所以我们要将对于的连接文件描述符关闭。代码如下void Start() { while (true) { // signal(SIGCHLD, SIG_IGN); // 最佳实践 struct sockaddr_in clientaddr; socklen_t len sizeof(clientaddr); // 建立连接 int sockfd accept(_lisentsockfd, (struct sockaddr *)clientaddr, len); if (sockfd) { LOG(LogLevel: :WARNING) accept errr!; continue; } LOG(LogLevel::DEBUG) accept success,sockfd: sockfd; // 处理链接 //单进程版本 // InetAddr clientaddress(clientaddr); // serviceIO(sockfd, clientaddress); //2、多进程版本 pid_t id fork(); if(id 0) { LOG(LogLevel::FATAL) fork errr!; exit(FORK_ERR); } else if(id 0) { // 关闭掉自己不需要的sockfd close(_listensockfd); // 子进程 if(fork() 0) exit(0); // 子进程直接退出 // 孙子进程 - 孤儿进程 InetAddr clientaddress(clientaddr); serviceIO(sockfd, clientaddress); close(sockfd); exit(0); // 重要 } else{ // 父进程 // wait(id); waitpid: WNOHANG // 关闭掉自己不需要的sockfd close(sockfd); pid_t rid waitpid(id, nullptr, 0); (void)rid; } close(sockfd); } }上面我们完成的是从单进程到多进程的改写那么创建进程的开销相对于创建线程还是比较大的所以我们还可以将其改成多线程的方式。那么这样效率就高很多了。但是还有个疑问就是当我们创建新线程那么我们的主线程不也要对新创建的线程进行回收么?所以我们可以将新线程设置成分离状态那么我们主线程也就不用进行join了。然后对于线程的处理函数我们知道其参数只能是一个返回值为void*参数也为void*的函数那么我们在类中可以将其设置成static静态全局的但是又有个问题了就是在这个函数中我们又无法获取到this指针了那么我们可以选择在这个服务端类中在创建一个类这个类中我们设置一个服务端指针然后在线程任务的时候将这个指针进行传入那么我们就可以得到this指针了那么在该函数中就可以获取到服务端方法了。代码如下class ThreadData { public: ThreadData(TcpServer *ts, int sockfd, const InetAddr addr) : _this(ts), _sockfd(sockfd), _addr(addr) { } ~ThreadData() { close(_sockfd); } public: TcpServer *_this; int _sockfd; InetAddr _addr; }; static void *thread_routine(void *args) { ThreadData *td static_castThreadData *(args); pthread_detach(pthread_self()); td-_this-serviceIO(td-_sockfd, td-_addr); delete td; return nullptr; }//ersion 2: 多线程版本 pthread_t tid; InetAddr clientaddress(clientaddr); ThreadData *td new ThreadData(this, sockfd, clientaddress); pthread_create(tid, nullptr, thread_routine, (void *)td);但是也要补充一点就是未来我们服务端其是一个长服务那么如果我们如上一直创建多线程那么我们的OS压力会很大我们上面的方式只合适在短服务上进行对于长服务我们后续会学习一个多路转发的方式。所以不论是多线程还是多进程方式我们上面的方式都只适合使用在多连接短服务上所以我们将IO服务进行一个修改我们的线程当执行了信息收发后然后马上退出当其要继续进行收发然后其再重新进行申请。对于这种短链接短服务可能我们创建线程的花销都要比处理任务的花销要大所以我们可以引入我们前面写的线程池。那么就这样就可以将我们的任务通过线程池进行调用完成。而且当任务过多的时候也会将其放入到任务队列中在合适的时候再进行调用。
返回列表