简单的TCP网络程序
TCP与UDP的区别:是否需要链接,通信的数据类型也不同
TCP需要链接,通信数据是面向字节流
TCP通用服务端
tcp服务端初始化
与udp服务端不同的是,tcp服务需要先进行链接
举个栗子:
当你向客服进行询问,前提是客服随时都在线,即使没有客户进行询问时也必须在线,将这种状态称为“监听”;tcp服务端也是如此,在进行链接之前需要将socket进行监听(第二个参数后面会介绍)
int listen(int sockfd, int backlog);
// 3.设置socket为监听状态 if (listen(_listensock, gbacklog) < 0) { std::cerr << "listen socket error" << std::endl; exit(LISTEN_ERR); } std::cout << "listen socket success!" << std::endl;
tcp服务端启动
接下来就是建立链接的过程,再举个栗子
在赌场周围会有拉客的人,称他们为张三;他们的目的就是拉客,将你拉入赌场进行消费,而当你进入赌场之后,真正为你服务的却不是那群拉客的人,而是里面的工作人员,称作李四;这里的张三的作用就只用来拉客,李四才是真的服务人员;tcp服务端也是如此,进行监听的socket在链接成功之后,会返回一个新的socket,新生成的socket的作用才是用来通信的;由于tcp通信是面向字节流,所以在链接成功之后,接下来的通信本质其实就是文件操作(IO操作)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 4.server获取新链接 struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); if (sock < 0) { std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl; continue; } std::cout << "accept success" << std::endl;
tcp服务端完整代码
namespace server { enum { USAGE_ERR = 1, SOCKET_ERR, BIND_ERR, LISTEN_ERR }; static const uint16_t gport = 8080; static const int gbacklog = 5; class TcpServer; class ThreadData { public: ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock) { } public: TcpServer *_self; int _sock; }; class TcpServer { public: TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port) { } ~TcpServer() { } void InitServer() { // 1.创建socket文件套接字 _listensock = socket(AF_INET, SOCK_STREAM, 0); if (_listensock < 0) { std::cerr << "create socket error" << errno << ":" << strerror(errno) << std::endl; exit(SOCKET_ERR); } std::cout << "create socket success!" << std::endl; // 2.bind绑定自己的网络信息 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; if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0) { std::cerr << "bind socket error" << errno << ":" << strerror(errno) << std::endl; exit(BIND_ERR); } std::cout << "bind socket success!" << std::endl; // 3.设置socket为监听状态 if (listen(_listensock, gbacklog) < 0) { std::cerr << "listen socket error" << std::endl; exit(LISTEN_ERR); } std::cout << "listen socket success!" << std::endl; } void start() { for (;;) { // 4.server获取新链接 struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(_listensock, (struct sockaddr *)&peer, &len); if (sock < 0) { std::cerr << "accept error,next" << errno << ":" << strerror(errno) << std::endl; continue; } std::cout << "accept success" << std::endl; // 多线程版 pthread_t tid; ThreadData *td = new ThreadData(this, sock); pthread_create(&tid, nullptr, thread_routine, td); } } static void *thread_routine(void *args) { pthread_detach(pthread_self()); ThreadData *td = static_cast<ThreadData *>(args); td->_self->serviceIO(td->_sock); close(td->_sock); delete td; return nullptr; } void serviceIO(int sock) { char buffer[1024]; while (true) { ssize_t n = read(sock, buffer, sizeof(buffer) - 1); if (n > 0) { // 目前为止将读到的数据当成字符串 buffer[n] = 0; std::cout << "read message:" << buffer << std::endl; std::string outbuffer = buffer; outbuffer += "server[echo]"; write(sock, outbuffer.c_str(), outbuffer.size()); } else if (n == 0) { // 数据被读取完毕,客户端退出 std::cout << "client quit,me too!" << std::endl; break; } } close(sock); } private: int _listensock; uint16_t _port; }; }
TCP通用客户端
tcp客户端初始化
tcp客户端初始化
这个过程与udp一样,不加赘述
void Initclient() { // 1.创建socket _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { std::cerr << "socket create error" << errno << strerror(errno) << std::endl; exit(1); } }
tcp客户端启动
既然服务端是监听,那么之后客服端发起链接之后,二者才能进行链接,然后通信
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "socket connect error" << std::endl; } else { std::string msg; while (true) { std::cout << "Enter#"; std::getline(std::cin, msg); write(_sock, msg.c_str(), msg.size()); char buffer[NUM]; int n = read(_sock, buffer, sizeof(buffer) - 1); if (n > 0) { // 目前为止将读取的数据当成字符串 buffer[n] = 0; std::cout << "Server回显#" << buffer << std::endl; } else { break; } } }
tcp客户端完整代码
#define NUM 1024 namespace client { class TcpClient { public: TcpClient(const std::string &serverip, const uint16_t &serverport) : _sock(-1), _serverip(serverip), _serverport(serverport) { } void Initclient() { // 1.创建socket _sock = socket(AF_INET, SOCK_STREAM, 0); if (_sock < 0) { std::cerr << "socket create error" << errno << strerror(errno) << std::endl; exit(1); } } void start() { struct sockaddr_in server; memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(_serverport); server.sin_addr.s_addr = inet_addr(_serverip.c_str()); if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0) { std::cerr << "socket connect error" << std::endl; } else { std::string msg; while (true) { std::cout << "Enter#"; std::getline(std::cin, msg); write(_sock, msg.c_str(), msg.size()); char buffer[NUM]; int n = read(_sock, buffer, sizeof(buffer) - 1); if (n > 0) { // 目前为止将读取的数据当成字符串 buffer[n] = 0; std::cout << "Server回显#" << buffer << std::endl; } else { break; } } } } ~TcpClient() { if (_sock >= 0) close(_sock); } private: int _sock; std::string _serverip; uint16_t _serverport; }; }
tcp通信展示
这里还存在一个问题,服务器运行之后,之前的任何指令都会被当做消息被发送,不会再被命令行解释器执行;通过键盘可直接将其停止;真正的服务器应该是保存在云端,一直运行不会受其他因素影响
接下来接受守护进程来解决这个问题
守护进程
xshell链接远端服务器之后,服务器立刻形成一个会话:有且只有一个前台任务,多个后台任务;其中bash命令行解释器一般作为前台任务,用来执行各种指令
通过命令行解释器形成两个后台任务;仔细观察会发现:两个任务分别是由两个组长(31271,31416)带领的,PGID为组长的代号;所有成员的共同领导是前台bash解释器(30990)
如果将其中一个后台任务,转换到前台结果会怎么样呢?
由于Bash命令行解释器被切换为了后台,所以各种指令任务一都无法执行;和上面的情形一致,守护进程是自称会话,相当于自己既是领导也是组长同时也是员工
模拟实现守护进程
1.使调用进程忽略异常的信号
signal(SIGPIPE, SIG_IGN);
如何服务端出现异常,客服端向其发送消息时会直接忽略掉异常,可以继续发送
2.只有组员才能自成会话,setsid
举个栗子,在公司里面组长不允许直接离职创业,因为他需要管理下面的员工,但是员工就可以直接离职创业;这里采取的方式是,父进程退出,子进程自成会话
if (fork() > 0) exit(1); // 子进程-》守护进程 本质也是孤儿进程的一种 pid_t id = setsid(); assert(id != -1);
3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件
进程默认会打开0,1,2文件描述符所指向的文件,为确保服务器不受器影响,需要将其关闭;这里采取的是将其重定向到”文件黑洞“,可以接受所有指令
int fd = open(DEV, O_RDWR); if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); } else { close(0); close(1); close(2); }
完整代码
#define DEV "/dev/null" void deamonSelf() { // 1.使调用进程忽略异常的信号 signal(SIGPIPE, SIG_IGN); // 2.只有组员才能自成会话,setsid if (fork() > 0) exit(1); // 子进程-》守护进程 本质也是孤儿进程的一种 pid_t id = setsid(); assert(id != -1); // 3.守护进程是脱离终端的,关闭或者重定向之前进程默认打开的文件 int fd = open(DEV, O_RDWR); if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); } else { close(0); close(1); close(2); } }
服务端运行之后,确实立刻自成会话
查看服务端进程,被1号进程领养,自此网络通信告一段落
TCP协议通讯流程
服务器初始化:
调用socket, 创建文件描述符;
调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;
connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手