【Linux网络篇】:简单的TCP网络程序编写以及相关内容的扩展
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客
文章目录
- 一.简单的TCP网络程序
- 相关接口
- 代码实现
- 服务器单进程版
- 服务器多进程版
- 服务器多线程版
- 服务器线程池版
- 应用场景---英译汉
- 守护进程化
- 二.补充内容
- TCP建立连接(三次握手)
- TCP断开连接(四次挥手)
- TCP通信的全双工特性
一.简单的TCP网络程序
相关接口
1.socket
函数
int socket(int domain, int type, int protocol);
- 功能:创建套接字
- 参数:
domain
:协议族,比如AF_INET
(IPv4);type
:套接字类型,SOCK_DGRAM
(UDP),SOCK_STREAM
(TCP);protocol
:协议,通常为0;
- 返回值:成功返回套接字描述符
sockfd
(类似于文件描述符),后续所有的操作都依赖这个描述符;失败返回-1。
2.bind
函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
功能:将套接字与特定的IP地址和端口绑定
-
参数:
sockfd
:套接字描述符addr
:地址结构体指针addrlen
:地址结构体长度
-
返回值:成功返回0,失败返回-1
-
使用示例:
struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(8080); // 端口号 local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意IPif (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {perror("bind error");return -1; }
3.listen
函数
int listen(int sockfd, int backlog);
- 作用:服务端将套接字设置为监听状态,等待客户端连接
- 参数:
sockfd
:套接字描述符backlog
:等待连接队列的最大长度
- 返回值:成功返回0,失败返回-1。
4.accept
函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 作用:服务端接受客户端的连接请求
- 参数:
sockfd
:监听套接字描述符addr
:用于存储客户端地址信息的结构体指针addrlen
:地址结构体的长度
- 返回值:成功返回新的套接字描述符,失败返回-1。
5.connect
函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:客户端用于连接服务器
- 参数:
sockfd
:套接字描述符addr
:服务器地址信息addrlen
:地址结构体的长度
- 返回值:成功返回0,失败返回-1
7.write
函数
ssize_t write(int fd, const void *buf, size_t count);
- 作用:向文件描述符写入数据(因为TCP是面向字节流的,所以可以用该函数发送数据)。
- 参数:
fd
:文件描述符(套接字描述符)buf
:要发送的数据缓冲区count
:要发送的字节数
- 返回值:成功返回实际写入的字节数;失败返回-1,并设置
errno
。
8.read
函数:
ssize_t read(int fa, void *buf, size_t count);
- 作用:从文件描述符读取数据(因为TCP是面向字节流的,所以可以用该函数接收数据)。
- 参数:
fd
:文件描述符(套接字描述符)buf
:接收数据的缓冲区count
:缓冲区的大小
- 返回值:成功返回实际读取的字节数;失败返回-1,并设置
errno
。
9.close
函数
int close(int sockfd);
- 功能:关闭套接字
- 参数:
sockfd
:要关闭的套接字描述符
- 返回值:成功返回0;失败返回-1。
使用这些函数的基本流程:
服务端:
1.创建套接字(socket)
2.绑定套接字(bind)
3.开始监听(listen)
4.接受连接(accept)
5.接收数据(read)
6.发送数据(write)
7.关闭连接(close)
客户端:
1.创建套接字(socket)
2.连接服务器(connect)
3.发送数据(write)
4.接收数据(read)
5.关闭连接(close)
注意点:客户端并没有绑定套接字的步骤,不代表客户端不用绑定,只是不需要用户来绑定而已,这一过程是由系统来完成的。因为客户端的主要目的是连接服务器,而不是被其他的程序连接,不需要一个固定的,众所周知的端口号,系统会自定分配一个可用的临时端口号。
代码实现
主程序:
用来启动服务器
#include "tcpserver.hpp"
#include <iostream>
#include <memory>void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " port[1024+]\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 2){Usage(argv[0]);exit(0);}uint16_t serverport = std::stoi(argv[1]);std::unique_ptr<TCPServer> tcp_svr(new TCPServer(serverport));tcp_svr->InitServer();tcp_svr->Run();return 0;
}
客户端:
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SIZE 4096
enum{SOCK_ERR=1,CONNECT_ERR,
};void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 3){Usage(argv[0]);exit(0);}// 获取服务端的IP地址和端口号std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服务端的网络地址结构体struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));// 创建client socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "client socket create error..." << std::endl;exit(SOCK_ERR);}// 连接bind client socket 由系统完成bind 随机端口// 客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if(n < 0){std::cerr << "client connect error..." << std::endl;exit(CONNECT_ERR);}// 进行通信std::string message;while(true){std::cout << "Please Enter@ ";getline(std::cin, message);// 发送信息到服务端write(sockfd, message.c_str(), message.size());// 接收处理后的信息char buffer[SIZE];ssize_t k = read(sockfd, buffer, sizeof(buffer));if(k > 0){buffer[k] = 0;std::cout << buffer << std::endl;}else if(k == 0){std::cout << "client quit!" << std::endl;//break;}else{std::cout << "client read error!" << std::endl;//break;}}// 关闭套接字描述符close(sockfd);return 0;
}
服务器单进程版
#pragma once#include "log.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>#define MAXSIZE 4096Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}void Run(){log(INFO, "TCPServer is running...");while(true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版Service(sockfd, clientip, clientport);close(sockfd);}}~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
编译后启动服务器和客户端进行通信测试
测试现象:
先启动右上角第一个客户端进行测试,服务端以及客户端都能正常收到信息;但是当打开右下角第二个客户端进行测试时,发送两条信息后,服务端并没有正常收到信息,以及客户端也没有收到处理后的信息,而一旦把第一个客户端关闭后,第二个客户端之前发送的信息,服务端以及第二个客户端就会立即收到。
原因:
目前服务端的根据新链接建立通信这一块使用的是单进程实现的,并且是在一个循环中,只有第一个启动的客户端退出后,才能继续进入下一次的循环,重新获取新链接建立通信。所以这就是为什么第二个启动的客户端发送的信息,刚开始时服务端并没有收到,而是等到第一个客户端退出后才收到信息。
解决方法也有很多种,有多进程,多线程等方法实现。
服务器多进程版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);version 2 --- 多进程版pid_t id = fork();if(id == 0){// childclose(_listensockfd);// 子进程再创建一个新的孙子进程 然后子进程立即退出if(fork() > 0){exit(0);}// grandson// 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收Service(sockfd, clientip, clientport);close(sockfd);exit(0);}// father// 父进程关闭sockfd 然后就只剩下子进程使用sockfdclose(sockfd); // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...pid_t rid = waitpid(id, nullptr, 0); } }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
测试:
服务器多线程版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer;class _ThreadData{
public:_ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t):sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t){}public:int sockfd_;std::string ip_;uint16_t port_;TCPServer *tsvr_;
};class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}static void *Routine(void *args){// 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程pthread_detach(pthread_self());_ThreadData *td = static_cast<_ThreadData *>(args);// 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);// 释放delete td;return nullptr;}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);// version 2 --- 多进程版// pid_t id = fork();// if(id == 0){// // child// close(_listensockfd);// // 子进程再创建一个新的孙子进程 然后子进程立即退出// if(fork() > 0){// exit(0);// }// // grandson// // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收// Service(sockfd, clientip, clientport);// close(sockfd);// exit(0);// }// // father// // 父进程关闭sockfd 然后就只剩下子进程使用sockfd// close(sockfd); // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...// pid_t rid = waitpid(id, nullptr, 0); // version 3 --- 多线程版本pthread_t tid;ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);pthread_create(&tid, nullptr, Routine, td);} }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
测试:
使用多线程实现,相当于每有一个新的链接,就要创建一个新的线程,所以新线程可能会越来越多
服务器线程池版
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"
#include "threadpool.hpp"
#include "Task.hpp"#define MAXSIZE 4096extern Log log;enum
{PORT_ERR=1,SOCKET_ERR,BIND_ERR,LISTER_ERR
};int backlog = 10;const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";class TCPServer;class _ThreadData{
public:_ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t):sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t){}public:int sockfd_;std::string ip_;uint16_t port_;TCPServer *tsvr_;
};class TCPServer{
public:TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip): _listensockfd(0), _port(serverport), _ip(serverip){if(_port < 1024){log(Warning, "Port number %d is too low, please use a port number > 1024", _port);exit(PORT_ERR);}}void InitServer(){// 1.创建tcp socket_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if(_listensockfd < 0){log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));exit(SOCKET_ERR);}log(INFO, "server socket create success, listensockfd: %d", _listensockfd);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);inet_aton(_ip.c_str(), &(local.sin_addr));// 2.连接tcp socketif(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(BIND_ERR);}log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);// 3.监听listen socket// TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态if(listen(_listensockfd, backlog) < 0){log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));exit(LISTER_ERR);}log(INFO, "server listen success, listensockfd: %d", _listensockfd);}static void *Routine(void *args){// 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程pthread_detach(pthread_self());_ThreadData *td = static_cast<_ThreadData *>(args);// 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);// 释放delete td;return nullptr;}void Run(){log(INFO, "TCPServer is running...");// 启动线程池ThreadPool<Task>::GetInstance()->start();while (true){// 1.获取新链接struct sockaddr_in client;socklen_t len = sizeof(client);// _listensockfd是用来监听的 后续的通信都是使用新的sockfdint sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);if(sockfd < 0){log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);// 2.根据新链接建立通信// version 1 --- 单进程版// Service(sockfd, clientip, clientport);// close(sockfd);// version 2 --- 多进程版// pid_t id = fork();// if(id == 0){// // child// close(_listensockfd);// // 子进程再创建一个新的孙子进程 然后子进程立即退出// if(fork() > 0){// exit(0);// }// // grandson// // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收// Service(sockfd, clientip, clientport);// close(sockfd);// exit(0);// }// // father// // 父进程关闭sockfd 然后就只剩下子进程使用sockfd// close(sockfd); // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...// pid_t rid = waitpid(id, nullptr, 0); // version 3 --- 多线程版本// pthread_t tid;// ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);// pthread_create(&tid, nullptr, Routine, td);// version 4 --- 线程池版本Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);} }~TCPServer(){if(_listensockfd > 0){close(_listensockfd);}}private:void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){// 测试代码char buffer[MAXSIZE];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);break;}}}private:int _listensockfd;uint16_t _port;std::string _ip;
};
线程池封装
#pragma once #include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <queue>
#include <unistd.h>struct ThreadData{pthread_t _tid;std::string _threadname;
};template<class T>
class ThreadPool{static const int defaultnum = 5;private:// 申请锁封装void Lock(){pthread_mutex_lock(&_mutex);}// 释放锁封装void UnLock(){pthread_mutex_unlock(&_mutex);}// 唤醒封装void WakeUp(){pthread_cond_signal(&_cond);}// 条件等待封装void ThreadSleep(){pthread_cond_wait(&_cond, &_mutex);}// 判断任务队列是否为空封装bool IsTaskEmpty(){return _tasks.empty();}std::string GetThreadName(pthread_t tid){for(auto td : _threads){if(td._tid == tid){return td._threadname;}}return "NONE";}public:// 线程的执行函数在类内要设置成静态成员函数static void *HandlerTask(void *args){// 错误写法:线程的执行函数设置成静态成员函数后,就不能再直接访问类内的成员变量和成员函数// while(true){// Lock();// while(_task.empty()){// ThreadSleep();// }// T t = _task.front();// _task.pop();// UnLock();// t.run(); // 每个任务对象在类内有自己的执行方法,所以不用在临界区执行// }// 正确写法:ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();// 循环判断条件,防止伪唤醒while(tp->IsTaskEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->UnLock();t.Run();}}void start(){int size = _threads.size();for (int i = 0; i < size; i++){_threads[i]._threadname = "thread-" + std::to_string(i + 1);//pthread_create(&(_threads[i].tid), nullptr, HandlerTask, nullptr);// 为了方便线程执行函数直接访问成员变量和成员函数,这里将this指针作为参数传过去pthread_create(&(_threads[i]._tid), nullptr, HandlerTask, this);}}// 存放任务到任务队列中void Push(const T&t){Lock();_tasks.push(t);WakeUp();UnLock();}// 从任务队列中获取任务T Pop(){T t = _tasks.front();_tasks.pop();return t;}// 懒汉方式单例模式// 静态成员函数只能访问静态成员变量 静态成员变量只有一份,在多线程情况下,就变成了共享资源// 需要加上互斥机制进行保护static ThreadPool<T>* GetInstance(){// 因为多线程只会在初次创建单例时才会竞争_tp指针 如果直接在判断外面加锁// 会导致创建之后每次使用单例时都要申请锁释放锁 所以采用双层判断空指针 降低锁冲突的概率 提高性能if (_tp == nullptr){pthread_mutex_lock(&_s_mutex);if (_tp == nullptr){_tp = new ThreadPool<T>();}pthread_mutex_unlock(&_s_mutex);}return _tp;}private:ThreadPool(int num = defaultnum):_threads(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}// 禁用拷贝构造函数和赋值运算符// 禁止通过拷贝构造函数创建新的线程池对象ThreadPool(const ThreadPool<T> &) = delete;// 禁止通过赋值运算符将一个线程池对象那个赋值给另一个const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;private:std::vector<ThreadData> _threads; // 线程数组std::queue<T> _tasks; // 任务队列pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *_tp; // 静态成员变量static pthread_mutex_t _s_mutex;
};template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr; // 静态成员变量只能在类外初始化template <class T>
pthread_mutex_t ThreadPool<T>::_s_mutex = PTHREAD_MUTEX_INITIALIZER;
任务封装
#pragma once#include <iostream>
#include <string>
#include "log.hpp"extern Log log;class Task{
public:Task(const int sockfd, const std::string ip, const uint16_t port): _sockfd(sockfd), _ip(ip), _port(port){}void Run(){char buffer[4096];while(true){ssize_t n = read(_sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;// 信息处理std::cout << "server get a message: " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;// 信息发送n = write(_sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){log(INFO, "%s:%d quit, server close sockfd: %d", _ip.c_str(), _port, _sockfd);}else{log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", _sockfd, _ip.c_str(), _port);}}}~Task(){}
private:int _sockfd;std::string _ip;uint16_t _port;
};
效果和多线程实现的一样,这里就不再展示。
应用场景—英译汉
直接使用线程池版的服务器来实现:
dict.txt文件
该文件用来存放进行翻译的单词(可以自己添加对应的单词)
apple:苹果
banana:香蕉
red:红色
Init.hpp
用来封装英译汉功能:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "log.hpp"const std::string dictname = "./dict.txt";
const std::string sep = ":";static bool Split(std::string &line, std::string *part1, std::string *part2){auto pos = line.find(sep);if(pos == std::string::npos){return false;}// 输出型参数*part1 = line.substr(0, pos);*part2 = line.substr(pos + 1);return true;
}class Init{
public:Init(){std::ifstream in(dictname);if(!in.is_open()){log(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}std::string line;while (std::getline(in, line)){std::string part1, part2;Split(line, &part1, &part2);dict.insert({part1, part2});}in.close();}std::string translation(const std::string &key){auto iter = dict.find(key);if (iter == dict.end()){return "UnKnow";}else{return iter->second;}}private:std::unordered_map<std::string, std::string> dict;
};
修改Task.hpp:
将服务端对信息的处理转变为翻译:
测试:
注意点:
如果服务器处理完信息后正在向客户端写入时,此时客户端正好关闭,可能会导致服务器写入失败问题,所以在Task.hpp
中需要对服务器写入做异常处理。
如果服务器写入失败,可能会导致服务器的崩溃以及被系统杀掉,所以在服务器中需要对异常信号做忽略处理:
模拟实现客户端重连现象
为客户端添加一个重连模块:
如果客户端正在通信时,服务器关闭,客户端会重新进行链接,如果链接一定次数后还是失败,就会终止退出。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#define SIZE 4096
enum{SOCK_ERR=1,CONNECT_ERR,
};void Usage(std::string proc){std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}int main(int argc, char *argv[]){if(argc != 3){Usage(argv[0]);exit(0);}// 获取服务端的IP地址和端口号std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 填充服务端的网络地址结构体struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){// 创建client socketint sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "client socket create error..." << std::endl;exit(SOCK_ERR);}int cnt = 5;int isreconnect = false;// 连接bind client socket 由系统完成bind 随机端口do{// 客户端发起connect的时候,进行自动随机bindint n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "client connect error..., reconnect: "<< cnt << std::endl;sleep(2);}else{break;}} while (cnt && isreconnect);if(cnt == 0){std::cerr << "user ofline..." << std::endl;break;}// 进行通信std::string message;while (true){std::cout << "Please Enter@ ";getline(std::cin, message);// 发送信息到服务端ssize_t s = write(sockfd, message.c_str(), message.size());if (s < 0){std::cout << "client write error!" << std::endl;continue;}// 接收处理后的信息char buffer[SIZE];ssize_t k = read(sockfd, buffer, sizeof(buffer));if (k > 0){buffer[k] = 0;std::cerr << buffer << std::endl;}else if (k == 0){std::cout << "client reconnect!" << std::endl;break;}else{std::cerr << "client read error!" << std::endl;break;}}// 关闭套接字描述符close(sockfd);}return 0;
}
守护进程化
1.前台进程和后台进程:
在Linux中每个用户登录时,都会创建一个会话框session
,每个session
都包含一个bash
进程(命令行解释器),刚开始时,bash
进程就是前台进程。一个session
只能有一个前台进程在运行,键盘信号只能发给前台进程;至于后台进程无法从标准输入获取数据。
谁拥有键盘文件,谁就是前台进程;而前台进程和后台进程都可以向显示器文件打印。
- 以前台方式执行
./process
可以被键盘输入的ctrl+c
终止掉。
- 以后台方式执行
./process &
后台进程也是向显示器文件打印,为了防止干扰,将打印内容重定向到文件中
启动时前面的[1]
表示后台任务号。
- 查看所有的后台进程
jobs
- 将后台进程转变为前台进程
fg 后台任务号
- 暂停后台进程
先将后台进程变为前台进程,然后键盘输入ctrl+z
:
- 取消暂停
bg 后台任务号
2.Linux的进程间关系:
在上面图中,启动两个后台进程,第二个是通过管道启动三个进程,打印对应的进程信息。
其中PGID
表示当前进程所属的进程组ID;SID
表示当前进程所属的session
对话框的ID。
对于./process
进程,一个进程就是一个进程组;而对于管道方式启动的三个sleep
进程,属于同一个进程组,并且他们的进程组ID还是第一个进程的PID
。
什么是进程组:
- 一个或多个进程的集合;
- 每个进程组都有一个唯一的进程ID;
- 进程组中的第一个进程成为组长进程;
- 组长进程的PID等于进程组的PGID;
3.进程组和任务之间的关系:
在Linux中,任务和进程是同一个概念。
一个任务可以有多个进程共同完成,也可以是一个进程独立完成;而进程组既可以包含一个进程也可以包含多个进程。
一个任务只能属于一个进程组,而一个进程组可以包含多个任务。
纠正之前的概念:前台进程,后台进程实际上应该叫做前台任务和后台任务。
4.守护进程化:
在同一个session
会话中执行的指令转变为进程后,每个进程的父进程都是bash
进程:
一旦关闭bash
进程后,重新登陆建立一个新的session
会话启动一个新的bash
进程,再次查看之前的后台任务:
这些后台进程并没有因为父进程bash
的退出而终止掉,而是继续执行,并且失去父进程后,被系统领养变成孤儿进程。
结论就是:这些后台进程会受到用户登录和退出的影响!因为父进程改变!
如果想要创建一个不受到任何用户登录和退出影响的进程—需要守护进程化!
守护进程是一种特殊的后台进程,具有以下特征:
- 在后台运行
- 没有控制终端(自成进程组自成会话,不受到任何用户登录和退出的影响)
- 生命周期长
- 系统启动时运行,系统关闭时终止
因为守护进程需要创建新的会话,自成一个会话,所以父进程也是系统。
实际上守护进程也是孤儿进程,只不过这种“孤儿化”是有意为之的,目的就是让进程完全脱离控制终端,在后台长期运行。
创建新的会话需要用到setsid
函数:
pid_t setsid(void);
- 主要功能:
- 创建一个新的会话
- 调用进程成为新会话的会话首进程
- 调用进程成为新进程组的组长进程
- 调用进程没有控制终端
- 调用规则:
- 调用进程不能是进程组的组长
- 如果调用进程是进程组的组长,调用会失败
- 所以通常要先fork创建子进程,在子进程中调用该函数创建新的会话
- 作用:
- 使进程完全脱离控制终端
- 使进程成为新会话的领导者
- 使进程成为新进程组的组长
- 确保进程在后台运行
- 返回值:
- 成功:返回新会话的ID
- 失败:返回-1,并设置errno
- 常见用途:
- 创建守护进程
- 实现后台服务
- 脱离终端控制
- 创建独立进程组
将服务器守护进程化:
daemon.hpp
:
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = ""){// 1. 创建子进程pid_t id = fork();if(id > 0){// 父进程直接终止退出exit(0);}// 2. 子进程创建新的会话setsid();// 3. 更改当前调用进程的工作目录if (!cwd.empty()){chdir(cwd.c_str());}// 4. 标准输入 标准输出 标准错误重定向到/dev/nullint fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}// 5. 忽略异常信号signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);
}
tcpserver.hpp
:
重新编译执行:
启动后的服务器的PID
,进程组PGID
,会话SID
都是同一个4725
,说明当前的服务器是自成进程组自成会话的一个后台进程。
因为这里启动时并没有设置指定的工作路径,所以启动后还是在当前调用进程的工作目录下。
查看一下服务器的三个标准流是否重定向到目标文件/dev/null
下;该文件是一个特殊的设备文件,可以理解为一个垃圾站,会自动丢弃写入的数据。
测试时,左边终端启动完服务器后关闭,然后在右边打开一个新的终端,启动客户端然后进行测试:
再通过指令查看之前的服务器:
通过信号直接杀掉服务器后再次查看,就找不到之前启动的服务器了:
如果不想手写守护进程化的代码,可以直接使用系统库中的daemon
函数:
函数原型:
int daemon(int nochdir, int noclose);
参数说明:
nochdir
:0
:将工作目录改为根目录"/"
;1
:保持当前工作目录不变;
noclose
:0
:将标准输入,输出,错误重定向到/dev/null
;1
:保持标准输入输出,错误不变;
二.补充内容
TCP建立连接(三次握手)
目的:让客户端和服务器双方都确认彼此的收发功能正常,建立可靠的连接。
步骤:
-
1.第一次握手:客户端—>服务器
客户端发送
SYN
(同步)包,告诉服务器“我要连接你”。 -
2.第二次握手:服务器—>客户端
服务器收到后,回复
SYN+ACK
包,表示“我收到了你的请求,我也准备好了”。 -
第三次握手:客户端—>服务器
客户端收到
SYN+ACK
后,再发一个ACK
包,表示“我也准备好了”。
结果:连接建立,双方可以开始通信了。
形象比喻:
- 客户端敲门(
SYN
) - 服务器回应“我在家,你是谁?”(
SYN+ACK
) - 客户端说“我是XX,咱们聊聊吧!”(
ACK
)
TCP断开连接(四次挥手)
目的:让双方都能优雅的关闭连接,确保数据都传完。
步骤:
-
1.第一次挥手:客户端—>服务器
客户端发送
FIN
包,表示“我没有数据要发了,可以断开了”。 -
2.第二次挥手:服务器—>客户端
服务器收到后,回复
ACK
包,表示“我知道了”。 -
3.第三次挥手:服务器—>客户端
服务器处理完自己的数据后,也发送
FIN
包,表示“我也没有数据要发了”。 -
4.第四次挥手:客户端—>服务器
客户端收到
FIN
后,回复ACK
包,表示“我知道了”。
结果:连接彻底关闭。
形象比喻:
- 客户端说”我说完了“(
FIN
) - 服务器点头“我知道了”(
ACK
) - 服务器说“我也说完了”(
FIN
) - 客户端点头“我知道了”(
ACK
)
总结口诀
- 三次握手:你来——我应——你再确认
- 四次挥手:你说完——我知道——我也说完——你知道
TCP通信的全双工特性
先用一个简单的类比来理解什么是TCP全双工通信:
想象两个人通过电话通话:
1.双方可以同时说话和听对方说话
2.不需要等待对方说完才能说话
3.双方都有独立的“说话通道”和“听通道”
在TCP通信中:
1.每个TCP连接都有两个独立的数据流:
- 一个从客户端到服务器
- 一个从服务器到客户端
2.这两个数据流可以同时工作,互不影响
3.不需要等待一个方向的数据传输完成才能开始另一个方向
而TCP通信的全双工特性与缓冲区机制密切相关:
1.每个TCP连接都有四个缓冲区:
- 发送方的发送缓冲区
- 发送方的接收缓冲区
- 接收方的发送缓冲区
- 接收方的接收缓冲区
2.缓冲区的作用:
- 发送缓冲区:存储待发送的数据
- 接收缓冲区:存储已接收但还未被应用程序读取的数据
3.为什么能实现全双工:
- 发送和接收使用不同的缓冲区
- 发送操作不会阻塞接受操作
- 接受操作不会阻塞发送操作
明白了上面的特性之后,再来思考一个问题:
TCP是面向字节流的,那如何保证,读取上来的数据,是“一个完整”的报文呢?
首先,我们使用的read
和write
函数只是拷贝函数。包括send
和recv
函数也是如此,这些函数并不是网络数据的收发函数,只是实现数据从用户到内核或者从内核到用户的拷贝。
先来理解发送数据:
write
发送数据实际上是将数据拷贝到发送方的发送缓冲区,具体决定网络收发的(什么时候发,发多少,出错了怎么办)是TCP协议决定的。
因为只有TCP协议了解当前网络的健康状态和接收方的接受能力(之后会讲解),所以必须由TCP协议决定网络数据的收发。
而TCP协议属于操作系统的网络模块部分,所以用户把数据交给TCP,本质上就是把数据交给操作系统。就和之前学习文件时的把数据拷贝到struct *file
指向的文件缓冲区里一样;把数据发出去,发送到对应端的网络里,这个过程也如同当初学习文件时,把数据从文件缓冲区刷新到磁盘一样;作用都是一样的。
再来理解接收数据:
read
接收数据实际上是将数据从接收方的接收缓冲区拷贝到用户自定义的缓冲区,而对方发送多少数据,什么时候发的,有没有一次性全部发送过来这些都是由操作系统(TCP协议)来决定的;至于接收方从接收缓冲区中读取时,读取多少,什么时候读取,一次性读取完还是分批次读取这些都是不确定的了!
所以在接收方的用户应用层中就必须保证把协议定好,这样才能更好的定义读取到的数据分析,保证读取到的是“一个完整”的报文!
所以接下来的学习内容就是用户应用层的协议!
以上就是关于TCP网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!