【Linux】网络编程套接字二

网络编程套接字二

  • 1.TCP网络编程
    • 1.1TCP Server服务端
    • 1.2 TCP Client客户端
  • 2.Server 多进程版本
    • 2.1普通版
    • 2.2 信号版
  • 3.Server 多线程版
  • 4.Server 线程池版
  • 5.日志函数重新设计
  • 6.守护进程
  • 7.TCP协议通讯流程
  • 8.TCP和UDP 对比

在这里插入图片描述

喜欢的点赞,收藏,关注一下把!在这里插入图片描述

1.TCP网络编程

TCP和UDP在编程接口上是非常像的,前面我们说过TCP是面向连接的,UDP我们上篇博客也写过了,我们发现UDP服务端客户端写好启动直接就发消息了没有建立连接。TCP是建立连接的,注定在写的时候肯定有写不一样的地方。具体怎么不一样,我们写代码的方式看一下。

1.1TCP Server服务端

#pragma once#include <iostream>
#include <string>using namespace std;enum
{USAGG_ERR = 1,
};class tcpServer
{
public:tcpServer(const uint16_t port) : _port(port), _listensock(-1){}void initServer(){}void start(){}~tcpServer(){}private:uint16_t _port;int _sock;
};

我们已经知道,云服务器不允许绑定公网IP,所以这里我们直接使用INADDR_ANY绑定任意IP,端口号自己指定就行了。

#include"tcpServer.hpp"
#include<memory>void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}// ./tcpserver port
int main(int argc,char* argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGG_ERR);}uint16_t serverport=atoi(argv[1]);unique_ptr<tcpServer> tsv(new tcpServer(serverport));tsv->initServer();tsv->start();return 0;
}

初始化服务器

进行网络通信首先要创建套接字。

在这里插入图片描述

不过今天这里,socket第二个参数我们要写成 SOCK_STREAM 对应TCP协议面向字节流。

void initServer()
{// 1.创建socket套接字_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){}}

_sock<0,说明套接字失败,那就没有必须进行了,但是我们这里封装一个日志函数。日志是有日志等级的。

操作系统也是有日志的。

cat /var/log/messages   //查看日志

日志等级有些是warning,error,fatal等

在这里插入图片描述

未来我们也想这些消息是以不同等级显示出来的,必须还要以特定的格式显示出来。

#pragma once#include<iostream>
#include<string>#define DUGNUM  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4void logMessage(int level,const std::string& message)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]std::cout<<message<<std::endl;//暂定
};

未来在输出消息的时候,消息都是规范化的。统一调用这个函数,可以往显示器上面打,也可以往文件中写。

这个日志函数不完整,我们先把TCP服务端客户端写完再来完善。

enum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,
};void initServer()
{// 1.创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "socket create error");exit(SOCKET_ERR);}logMessage(NORMAL, "socker create success");// 2.bind 绑定自己的网络消息 port和ipstruct 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; // 任意地址bind,服务器真实写法if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}     

前面UDP服务端初始这两步做完到这里就完了,但是TCP服务器是面向连接的,所以当别人给我发数据时候不能直接发数据,必须先和我建立连接,这就意味着服务器必须时时刻刻知道他向我发起连接请求。所以必须有第3步 设置socket 为监听状态(为了获取新连接)

在这里插入图片描述

backlog:底层全连接队列的长度,这个参数在后面TCP协议的时候说 。

static const int backlog = 5;enum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};void initServer()
{// 1.创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "socket create error");exit(SOCKET_ERR);}logMessage(NORMAL, "socker create success");// 2.bind 绑定自己的网络消息 port和ipstruct 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; // 任意地址bind,服务器真实写法if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3.设置socket为监听状态if (listen(_listensock, backlog) < 0) // backlog  底层链接队列的长度{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socker success");}  

初始化服务器:1.创建socket ,2.bind ,3.设置socket 为监听状态

启动服务器

TCP不能直接发数据 ,因为它是面向连接的。通信之前必须要先获取连接,因此首先要获取新连接。

在这里插入图片描述

从这个sockfd这里获取新连接。

accept函数后两个参数和recvfrom是一模一样的,这两个参数的含义也是一样的都是输入输出型函数,将来谁连的我,远端的客户端的ip和port是多少。所以需要这两个参数把客户端消息获取上来。

在这里插入图片描述

这些都不重要,最重要的是accept的返回值

在这里插入图片描述

成功时这个函数会从已接受的socket返回一个文件描述符!失败返回-1错误码被设置。

这里问题就来了,调用accept它的返回值也是文件描述符,而我们自己也建立一个文件描述符,那这两个文件描述符是什么意思?

下面举个例子理解:
今天我和一群朋友去杭州西湖旅游,玩累了准备找个地方吃饭,假设来了一个地方都是卖鱼的,王家鱼庄、李家鱼庄、张家鱼庄等等。每一家鱼庄门口都有一个拉客的人,张三是王家鱼庄的门口拉客的人。我们走着走着张三过来了,小哥小哥你们要不要吃饭啊,我们这里的鱼都是从西湖打上来的。我们感觉可以试试,于是张三就带我和我的朋友到王家鱼庄,到了门口张三就向大厅呼唤李四过来接客把我们带进去,李四过来招呼我们,给我们倒水介绍特殊菜。当我们在享受李四给我们带来的服务时,张三去那了?张三自己有自己的业务,他把我们招呼过来之后,转头就走了,又跑到路边找下一位客人了。当我和我的朋友在吃饭的期间,发现我们周边越来越热闹了,张三带着客人来然后在门口喊着让其他人招呼客人。李四给我们提供服务,王五给别的客人提供服务等等。张三一直干着这一件事情。

张三 : 拉客
李四、王五、赵六。。。:提供服务

张三就相当于我们传给accept的创建好的文件描述符,
李四、王五、赵六。。。就相当于accept返回文件描述符

一个服务器可能被多个客户端来连接,李四、王五、赵六。。。每一个都是对应一个文件描述符对外提供服务的, 未来我们一旦建立好连接,服务器不能用创建好的文件描述符和客户端通信,就好比不能用张三给客人提供服务,而应该让accept的返回值文件描述符来给用户提供服务。

class tcpServer
{
public://。。。void start(){for (;;){// 4.获取新链接//这个结构体用来获取谁连接的我struct sockaddr_in peer;socklen_t len = (sizeof(peer));//这个sock 用来和client进行通信的文件描述符int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;//获取连接失败,但不影响获取下一个连接}logMessage(NORMAL, "accpet a new link success);//cout << "sock: " << sock << endl;}//。。。
private:uint16_t _port;//int _sock;int _listensock;//不是用来进行数据通信的,它是用来监听连接的到来,获取新连接的
};

接下来就用这个sock和客户端进行通信了

void start()
{for (;;){// 4.获取新链接//这个结构体用来获取谁连接的我struct sockaddr_in peer;socklen_t len = (sizeof(peer));//这个sock 用来和client进行通信的文件描述符int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;//获取连接失败,但不影响获取下一个连接}logMessage(NORMAL, "accpet a new link success);//cout << "sock: " << sock << endl;//可以看到新的文件描述符// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!// version 1 这里我们后面会写好几个版本。因此先写第一个简单版本serverIO(sock);//用这个函数对外提供服务close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
}void serverIO(int &sock)
{char buffer[1024];while (true){// 读ssize_t n = read(sock, buffer, sizeof(buffer));if (n > 0)//返回读到多少字节{//目前我们把读到的数据当成字符串,截至目前buffer[n] = 0;cout << "recv message: " << buffer << endl;// 写string outbuffer = buffer;outbuffer += "server [respond]";write(sock, outbuffer.c_str(), outbuffer.size());}else if (n == 0)//读到文件末尾{// 代表clien退出logMessage(NORMAL, "client quit, me to!");break;}}
}

服务端完整代码

#pragma once#include "logMessage.hpp"#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using namespace std;enum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};const int backlog = 5;class tcpServer
{
public:tcpServer(const uint16_t port) : _port(port), _listensock(-1){}void initServer(){// 1.创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){logMessage(FATAL, "socket create error");exit(SOCKET_ERR);}logMessage(NORMAL, "socker create success");// 2.bind 绑定自己的网络消息 port和ipstruct 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; // 任意地址bind,服务器真实写法if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");// 3.设置socket为监听状态if (listen(_listensock, backlog) < 0) // backlog  底层链接队列的长度{logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socker success");}void start(){for (;;){// 4.获取新链接//这个结构体用来获取谁连接的我struct sockaddr_in peer;socklen_t len = (sizeof(peer));//这个sock 用来和client进行通信的文件描述符int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;//获取连接失败,但不影响获取下一个连接}logMessage(NORMAL, "accpet a new link success);//cout << "sock: " << sock << endl;//可以看到新的文件描述符// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!// version 1 这里我们后面会写好几个版本。因此先写第一个简单版本serverIO(sock);//用这个函数对外提供服务close(sock);//对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏}void serverIO(int &sock){char buffer[1024];while (true){// 读ssize_t n = read(sock, buffer, sizeof(buffer));if (n > 0)//返回读到多少字节{//目前我们把读到的数据当成字符串,截至目前buffer[n] = 0;cout << "recv message: " << buffer << endl;// 写string outbuffer = buffer;outbuffer += "server [respond]";write(sock, outbuffer.c_str(), outbuffer.size());}else if (n == 0)//读到文件末尾{// 代表clien退出logMessage(NORMAL, "client quit, me to!");break;}}}  ~tcpServer(){}private:// string _ip;uint16_t _port;int _listensock;
};
netstat -nltp  //查看处于监听的TCP

1.2 TCP Client客户端

#pragma once#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using namespace std;class tcpClient
{
public:tcpClient(const string &ip, const uint16_t &port): _serverip(ip), _serverport(port), _sockfd(-1){}void initClient(){}void run(){    }~tcpClient(){}private:string _serverip;uint16_t _serverport;int _sockfd;
};
#include"tcpClient.hpp"#include<memory>void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_ip local_port\n\n";
}// ./tcpClient serverip serverport
int main(int argc,char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}string serverip=argv[1];uint16_t serverport=atoi(argv[2]);unique_ptr<tcpClient> utc(new tcpClient(serverip,serverport));utc->initClient();utc->run();return 0;
}

初始化客服端

void initClient()
{// 1.创建socket套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){cerr << "socket fail" << endl;exit(2);}// 2.tcp的客户端要不要bind,要不要显示bind?  要bind,不需要显示bind// 要不是listen 监听? // 要不要accept? 
}

因为客户端和服务端通信需要【源ip ,目的ip】,【源端口,目的端口】,所以要bind。但是不需要显示bind,因为如果bind特定的端口,如果两个客户端都bind一样的端口,谁先启动谁成功bind,另一个就不能启动了。

下一个问题,我们的客户端要不要listen?
不需要,服务器 listen是因为有人要连接它,客户端是发起连接的。

那客户端要不要accept?
不需要,服务器accept也是因为有人要连接它,客户端是是发起连接的。

那客户端到底要什么呢?
要发起连接!

发现连接我们写到启动客户端里

启动客户端

在这里插入图片描述

第一个参数通过那个套接字发起连接
第二个参数你要向那个ip和port的服务端发起连接
第三个参数是这个结构体的长度

在这里插入图片描述

以前在udp是第一次sendto发现没有bind会调用bind绑定ip和port,而tcp这里是在connect会帮bind。

void run()
{// 2.发起链接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(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0){cerr << "socker connect fail" << endl;}else{string msg;while (true){// 发cout << "Please Enter# ";getline(cin, msg);write(_sockfd, msg.c_str(), msg.size());// 收char buffer[1024];ssize_t n = read(_sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;cout <<"server 回显: " <<buffer << endl;}else{break;}}}
}

客户端完整代码

#pragma once#include <iostream>
#include <string>
#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using namespace std;class tcpClient
{
public:tcpClient(const string &ip, const uint16_t &port): _serverip(ip), _serverport(port), _sockfd(-1){}void initClient(){// 1.创建socket套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){cerr << "socket fail" << endl;exit(2);}// 2.要不要bind,要不要显示bind?  要bind,不需要显示bind// 要不是listen 监听? 不需要// 要不要accept? 不需要// 自己是发送链接的一方}void run(){// 2.发起链接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(_sockfd, (struct sockaddr *)&server, sizeof(server)) != 0){cerr << "socker connect fail" << endl;}else{string msg;while (true){// 发cout << "Please Enter# ";getline(cin, msg);write(_sockfd, msg.c_str(), msg.size());// 收char buffer[1024];ssize_t n = read(_sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;cout <<"server 回显: " <<buffer << endl;}else{break;}}}}~tcpClient(){//这里也可以不主动关,我们知道文件描述符的生命周期随进程,客户端进程退了会自动帮关的if(_sockfd >= 0) close(_sockfd);}private:string _serverip;uint16_t _serverport;int _sockfd;
};

在这里插入图片描述

为什么服务这里打印出来的文件描述符是4呢?
因为默认打开三个文件,0,1,2被占了,3被listensock占了,所以这里打印的是4

netstat -ntap  //查看所有处于tcp的进程

在这里插入图片描述

ESTABLISHED :建立连接
我们确实看到客户端发起的连接已经被服务端看到了并且连接了。
这里的问题为什么有两条连接呢?正常情况下不是一条连接吗?
一般而言,TCP确实在查找的时候建立连接成功,只会有一条连接!!!
但是今天我们做测试,客户端和服务端是在一台机器上的!!!
如果是两台主机,你是服务端你看到的就是上面的,你是客户端你看到的是下面的。即便只有一条连接也是全双工的!

在这里插入图片描述
这里可以看到客户端关了服务端立马读到了,客户端在连这个文件又变成4了,这说明客户端一关闭服务端就将刚刚的文件描述符关了,关了之后你在连接我给你的还是4,此时文件描述符就被重复使用了。

在这里插入图片描述

注意看,当我又开一个客户端去连接然后给服务端发送消息的时候,服务端并不会显示,只有当我把上一个客户端关闭后,然后才获取到新连接,这个文件描述符还是4,才会把我发的消息接收。

这是因为刚才所写的服务器,我们获取一个新连接之后,然后进程就去serverIO提供死循环服务了。人家不退,服务器就一直在serverIO给人家提供服务。

在这里插入图片描述

那怎么能保证并发的,给多个人提供服务呢?

下面我们就把刚才写的版本改一下:
多进程两个版本,多线程版本,线程池版本。

2.Server 多进程版本

2.1普通版

在这里插入图片描述
获取新连接之后创建子进程,创建子进程,父进程的文件描述符会被子进程继承的,文件描述符所指的文件也都是一样的。所以说父进程曾经打开的listensock以及sock子进程都能看到。

创建子进程,让子进程对外提供服务。
这里要注意父进程的文件描述符被子进程继承下来了,但是父进程可是打开了多个文件描述符,所以子进程最少把自己的不需要的文件描述符关掉。

void start()
{for (;;){// 4.获取新链接struct sockaddr_in peer;socklen_t len = (sizeof(peer));int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;}// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);logMessage(NORMAL, "accpet a new link success,get new sock");cout << "sock: " << sock << endl;// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!// version2 多进程int fd=fork();if(fd == 0) //child{//关闭不需要的监听文件描述符close(_listensock);serverIO(sock);close(sock);exit(0);}//父进程
}

那父进程要干什么呢?
根据以前在进程哪里所学知识,父进程当然是要阻塞或者非阻塞等待回收子进程的资源了,否则子进程退出变成僵尸进程了,就造成内存资源泄漏了

在这里插入图片描述

但是这里要等待的时候,选择阻塞式等待还是非阻塞等待?

选择阻塞式等待,那不还是串行执行吗,属于脱裤子放屁多此一举创建子进程。选择非阻塞式等待,万一没有新连接来了一直在accept哪里等着连接,对子进程资源可能并没有回收干净造成内存资源泄漏。所以选择非阻塞式等待并不好!

如果非要让你阻塞式等待,要怎么做?

这里是这样做的,让子进程关闭listensock之后,子进程在创建一个子进程也就是孙子进程,让子进程退出!孙子进程提供服务。因为子进程退出了所以父进程等待会立马成功,然后继续向下执行代码。虽然父进程回收了子进程资源,但是并不影响孙子进程提供服务,等孙子进程提供完服务自己退出。你是孙子进程和父进程没有半毛钱关系(各管各儿子),孙子进程是一个孤儿进程,孤儿进程会被操作系统领养然后等它退了回收它。

void start()
{for (;;){//。。。// version2 多进程int fd=fork();if(fd == 0) //child{//关闭不需要的监听文件描述符close(_listensock);if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养serverIO(sock);close(sock);exit(0);}//父进程pid_t ret=waitpid(fd,nullptr,0);if(ret > 0){logMessage(NORMAL,"waitpid child success");}
}

完整代码

void start()
{for (;;){// 4.获取新链接struct sockaddr_in peer;socklen_t len = (sizeof(peer));int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;}// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);logMessage(NORMAL, "accpet a new link success,get new sock");cout << "sock: " << sock << endl;// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!// version2 多进程int fd=fork();if(fd == 0) //child{//关闭不需要的监听文件描述符close(_listensock);if(fork() > 0) exit(0);//创建孙子进程,让子进程退出,孙子进程变成孤儿进程被OS领养serverIO(sock);close(sock);exit(0);}//父进程pid_t ret=waitpid(fd,nullptr,0);if(ret > 0){logMessage(NORMAL,"waitpid child success");}
}

在这里插入图片描述

看到现在可以多个用户同时连接了。但是多进程并不是一个好方法,因此子进程要拷贝一份父进程的东西。

2.2 信号版

上面还需要父进程自己回收子进程的资源太麻烦,我们知道子进程退出并不是默默退出的,它会发17号信号,不过系统默认对这个信号是忽略。这些知识我们在信号哪里说过,不在叙述。

因此这里我们让子进程退出然后资源自动被回收。父进程自己忙自己的事情。

void start()
{//子进程退出自动被OS回收signal(SIGCHLD,SIG_IGN);for (;;){//。。。// version2 多进程信号版int fd=fork();if(fd == 0){close(_listensock);serverIO(sock);close(sock);exit(0);}}

这里有个问题,子进程关闭了不用的listensock文件描述符,父进程要不要关闭sock文件描述符?

在这里插入图片描述

父进程没关sock文件描述符,客户端关闭后再连接,文件描述符是一直增长的状态。文件描述符终有用完的时候!

所以父进程一定要关闭提供服务的sock文件描述符,虽然父进程关闭sock但它不会造成文件关闭,因为有引用计数,等到引用计数到0的时候这个文件才会真正的关闭!

void start()
{//子进程退出自动被OS回收signal(SIGCHLD,SIG_IGN);for (;;){// 4.获取新链接struct sockaddr_in peer;socklen_t len = (sizeof(peer));int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;}// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);logMessage(NORMAL, "accpet a new link success,get new sock");cout << "sock: " << sock << endl;// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!// version2 多进程信号版int fd=fork();if(fd == 0){close(_listensock);serverIO(sock);close(sock);exit(0);}//细节,子进程关闭父进程的,父进程关闭子进程的close(sock);      
}

在这里插入图片描述
这里可能会有端口绑定失败,原因在具体谈TCP协议再说!
我们先换个端口用。
在这里插入图片描述

3.Server 多线程版

现在我们想用线程来解决为多人提供服务。

创建新线程,那主线程和新线程之间多文件描述符的态度是什么?
这个sock文件描述符能不能被新线程看到呢?

能!它们共享同一份资源!这里也不用敢像多进程那样让父子进程关闭对应的文件描述符那样做。它们共享同一份资源!

新线程创建好了,主线程也要回收新线程的资源。以前用的是pthread_join,但是在后面我们学过可以使用pthread_deatch进行线程分离,主线程就不用等了。

剩下线程代码细节我们以前说过,这里不再细说。

class tcpServer;//声明struct ThreadDate
{ThreadDate(int sock,tcpServer* tps):_sock(sock),_tps(tps){}int _sock;tcpServer* _tps;
};class tcpServer
{
public:
{//。。/static void* start_routine(void* args){ThreadDate* td=static_cast<ThreadDate*>(args);pthread_detach(pthread_self());//退出自动回收资源td->_tps->serverIO(td->_sock);close(td->_sock);delete td;td=nullptr;}void start(){for (;;){// 4.获取新链接struct sockaddr_in peer;socklen_t len = (sizeof(peer));int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;}// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);logMessage(NORMAL, "accpet a new link success,get new sock");cout << "sock: " << sock << endl;// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!//version3  多线程pthread_t pid;//把this指针和sock一起传过去,因此写个结构体ThreadDate* td=new ThreadDate(sock,this);pthread_create(&pid,nullptr,start_routine,td);}}//。。。
}

4.Server 线程池版

思路是这样的,未来新连接来了,我们可以把新连接构成一个任务,然后放到线程池里,由线程池来进行统一处理。

线程池我们在 【liunx】线程池+单例模式+STL,智能指针和线程安全+其他常见的各种锁+读者写者问题 这里写过,并且做了封装,因此我们拿过来直接用。

线程封装

#pragma once
#include<iostream>
#include<string>
#include<pthread.h>
#include<functional>class Thread
{typedef  std::function<void*(void*)> func_t;
private://类内成员有隐藏的this指针,不加static就会报错!//但是我们又需要this指针,调用类的成员变量,因此把this传过来static void* start_routine(void* args){Thread* _this=static_cast<Thread*>(args);//安全进行类型转换return _this->_func(_this->_args);//调用回调函数,不这样写也可以再写一个类内函数在调用}
public:Thread(){char namebuffer[64];snprintf(namebuffer,sizeof namebuffer,"thread-%d",_number++);_name=namebuffer;}//为什么这里参数不放在构造函数//因为我们等会想线程运行的时候,知道是那个线程在运行把_name也一起传过去void start(func_t func,void* args){_func=func;_args=args;//这个函数不认识C++的function类,因此我自己写一个函数pthread_create(&_tid,nullptr,start_routine,this);}void join(){pthread_join(_tid,nullptr);}std::string threadname(){return _name;}~Thread(){}private:std::string _name;//线程名func_t _func;//回调函数void* _args;//回调函数参数pthread_t _tid;//线程IDstatic int _number;
};int Thread::_number=1;

锁封装

#pragma once
#include<iostream>
#include<pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* lock):_lock(lock){pthread_mutex_init(_lock,nullptr);}void lock(){pthread_mutex_lock(_lock);}void unlock(){pthread_mutex_unlock(_lock);}~Mutex(){pthread_mutex_destroy(_lock);}
private:pthread_mutex_t* _lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};

任务封装

#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <unistd.h>
#include "logMessage.hpp"
using namespace std;void serverIO(int sock)
{char buffer[1024];while (true){// 读ssize_t n = read(sock, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;cout << "recv message: " << buffer << endl;// 写string outbuffer = buffer;outbuffer += " server[respond]";write(sock, outbuffer.c_str(), outbuffer.size());}else if (n == 0){// 代表clien退出logMessage(NORMAL, "client quit, me to!");break;}}close(sock);//提供完自己关闭文件文件描述符
}class Task
{typedef std::function<void(int)> func_t;public:Task(){};Task(int sock, func_t func) : _sock(sock), _callback(func){}void operator()(){_callback(_sock);}private:int _sock;func_t _callback;
};

线程池单例封装

#pragma once
#include "Thread.hpp"
#include "Task.hpp"
#include <vector>
#include <queue>
#include "Mutex.hpp"
#include <mutex>using namespace std;
const int maxcap = 3;// 声明
template <class T>
class ThreadPool;template <class T>
class ThreadData
{
public:ThreadData(ThreadPool<T> *poolthis, const string &name) : _poolthis(poolthis), _name_(name){}~ThreadData(){}public:ThreadPool<T> *_poolthis;string _name_;
};template <class T>
class ThreadPool
{
private:// 线程调用的处理任务函数static void *handTask(void *args){ThreadData<T> *td = static_cast<ThreadData<T> *>(args);while (true){Task t;// RAII 风格加锁{// 构造时自动加锁,析构时自动结束// 局部变量生命周期这个代码块LockGuard lockguard(td->_poolthis->mutex());while (td->_poolthis->IsQueueEmpty()){td->_poolthis->threadwait();}td->_poolthis->pop(&t);}t(); //执行任务     }delete td;return nullptr;}private:void threadlock() { pthread_mutex_lock(&_lock); }void threadunlock() { pthread_mutex_unlock(&_lock); }void threadwait() { pthread_cond_wait(&_cond, &_lock); }void pop(T *out){*out = _task_queue.front();_task_queue.pop();}bool IsQueueEmpty() { return _task_queue.empty(); }pthread_mutex_t *mutex(){return &_lock;}// 单例不是没有例,构造函数不能去掉,放在private就好了ThreadPool(int cap = maxcap) : _cap(maxcap){// 初始化锁,条件变量pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);// 创建线程for (int i = 0; i < _cap; ++i){_threads.push_back(new Thread()); // 创建线程并放在vector里}}// 去掉赋值,拷贝构造void operator=(const ThreadPool &) = delete;ThreadPool(const ThreadPool &) = delete;public:// 启动线程// 在Thread里说过,想把线程名也传过去,但是回调函数只有一个函数// 而这函数我们写在类里面必须要加一个static,导致没有this指针,而使用类内成员需要this指针// 因此我们写个类把线程名和this都传过去void run(){for (auto &thread : _threads){ThreadData<T> *td = new ThreadData<T>(this, thread->threadname());thread->start(handTask, td);cout << thread->threadname() << " statr... " << endl;}}// 任务队列放任务void push(const T &in){// 保证放任务是安全的,所以先加锁pthread_mutex_lock(&_lock);_task_queue.push(in);pthread_cond_signal(&_cond); // 队列中有任务就唤醒等待的线程去取任务pthread_mutex_unlock(&_lock);}~ThreadPool(){// 销毁锁,条件变量pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}// 获取单例// 成员函数可以调用静态成员和静态成员函数,反之不行static ThreadPool<T> *getInstance(){// 虽然没有并发问题了,但是还有一个小问题// 未来每一个线程进来都要lock,unlock// 因此在外面再加一个if判断,未来只要第一次实例化之后就不需要再加锁解锁了// 大家就可以并发了if (tp == nullptr){_singlock.lock();if (tp == nullptr){tp = new ThreadPool<T>();}_singlock.unlock();}return tp;}private:int _cap;                  // 线程个数vector<Thread *> _threads; // 线程放在vector里进行管理queue<T> _task_queue;      // 任务队列pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T> *tp;// c++11的锁static std::mutex _singlock;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;template <class T>
mutex ThreadPool<T>::_singlock;

Server 线程池版

void start()
{//线程池启动ThreadPool<Task>::getInstance()->run();logMessage(NORMAL, "Thread init success");for (;;){// 4.获取新链接struct sockaddr_in peer;socklen_t len = (sizeof(peer));int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符if (sock < 0){logMessage(ERROR, "accpet error");continue;}// logMessage(NORMAL, "accpet a new link success,get new sock: %d",sock);logMessage(NORMAL, "accpet a new link success,get new sock");cout << "sock: " << sock << endl;// 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!//version4 线程池//放任务ThreadPool<Task>::getInstance()->push(Task(sock,serverIO));}
}

5.日志函数重新设计

前面我们只是把日志函数简单说了一下,现在加一些设计。

我们想用一下可变参数,未来调用这个函数的时候是准备像下面这样调用。
创建套接字成功,然后打印一下。就像printf函数一样,【日志等级】【时间戳】【pid】【格式化的消息】

在这里插入图片描述

// void logMessage(int level,const std::string& message)
// {
//     //[日志等级] [时间戳/时间] [pid] [message]
//     //[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
//     std::cout<<message<<std::endl;
// };void logMessage(int level,const char* format,...)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]}

如果自己写比较麻烦

// void logMessage(DEBUG, "hello %f, %d, %c", 3.14, 10, 'C');
void logMessage(int level, const char *format, ...)
{// [日志等级] [时间戳/时间] [pid] [messge]// [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]va_list start; //start是一个指针   va_start(start);//让start指向可变参数列表第一个参数while(*p){//p指向format的位置,如hswitch(*p){case '%':p++;if(*p == 'f') arg = va_arg(start, float);//让start提取一个float类型...}}va_end(start); //start指针变成nullptr
}

提可变参数列表参数,一般用下面的函数。

在这里插入图片描述

const char* level_to_string(int level)
{switch(level){case DUGNUM: return "DUGNUM";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";}
}//时间戳变成时间
char* timeChange()
{time_t now=time(nullptr);struct tm* local_time;local_time=localtime(&now);static char time_str[1024];snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \local_time->tm_min, local_time->tm_sec);return time_str;
}void logMessage(int level,const char* format,...)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024//获取时间char* nowtime=timeChange();char logprefix[NUM];snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());//char logconten[NUM];va_list arg;va_start(arg,format);vsnprintf(logconten,sizeof logconten,format,arg);std::cout<<logprefix<<logconten<<std::endl
}

在这里插入图片描述

我们也可以把这些日志信息放到文件中去,这里我们使用C++对文件操作

#pragma once#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>#define DUGNUM  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"const char* level_to_string(int level)
{switch(level){case DUGNUM: return "DUGNUM";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";}
}//时间戳变成时间
char* timeChange()
{time_t now=time(nullptr);struct tm* local_time;local_time=localtime(&now);static char time_str[1024];snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \local_time->tm_min, local_time->tm_sec);return time_str;
}void logMessage(int level,const char* format,...)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024//获取时间char* nowtime=timeChange();char logprefix[NUM];snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());//char logconten[NUM];va_list arg;va_start(arg,format);vsnprintf(logconten,sizeof logconten,format,arg);//写到文件if(level == DUGNUM || level == NORMAL || level == WARNING){std::ofstream oss1(LOG_NORMAL,std::ios_base::out|std::ios_base::app);oss1<<logprefix<<logconten<<std::endl;    }else{std::ofstream oss2(LOG_ERR,std::ios_base::out|std::ios_base::app);oss2<<logprefix<<logconten<<std::endl;   } 
};

在这里插入图片描述
在这里插入图片描述

6.守护进程

服务器启动没问题,但是我们把这个终端关掉,此时我们看到服务就没了。也就是说服务器启动了不能关闭xshell,一关就没了。

在这里插入图片描述

正常服务器肯定不是这样运行的。服务器启动之后不再受用户登录注销的影响,而服务器可以自由运行的,除非未来不想用它,Quit它。

在liunx中这种进程,叫做守护进程

我们xshell客户端连上远端的与服务器会有一个会话,会话内部会给我提供一个前台进程bash,然后用户在命令行中自由的启动前台或者后台的任务,在这个会话中,只允许一个前台任务,和0个或者多个后台任务。
在这里插入图片描述
后面加& ,将任务放到后台

在这里插入图片描述

这里打印出来的东西暂时不用管

在这里插入图片描述

然后我们在以后台方式启动几个任务

在这里插入图片描述

这就是对应的两个作业,作用编号1、2

在这里插入图片描述

然后我们查看当前进程sleep,可以看到PGID,前三个进程是一样的,后三个进程是一样的,并且都是第一个进程的PID,这里想表达的是它们分别属于不同进程组。相同PGID的是一个进程组,组长是第一个进程。然后每个组三个人合起来成为一个进程组干一个作业。

这里想说的是,任务(1、2、3)是由各个进程组来完成的。

在这里插入图片描述

这些后端任务都属于同一个会话,从SID全都是一样可以看到。会话是以bashID来命名这个会话的。

在这里插入图片描述

& 以后端方式起任务

jobs //查看当前会话

在这里插入图片描述

fg 2 //2号任务放前台

在这里插入图片描述

ctrl+z //暂停这个任务

一个任务在前台暂停了,立马会被放到后台

在这里插入图片描述

然后ls发现bash又回来了,这证明了有且只有一个前台任务。

在这里插入图片描述

把一个任务放前台,bash自动变后台,这也说明以前我们自己在以./ 启动任务,是把任务放到前台了,所以输入其他指令根本没用

在这里插入图片描述

bg 2 //启动2号任务

在这里插入图片描述

所以我们得到的结论就是,作业是可以前台转化的

这就是会话进程组作业之间的关系。

xshell登录的时候会建立这么一堆东西,那退出登录呢?
是不是所有任务都会自动清理。
在这里插入图片描述

所以我们要想不受用户登录注销的影响,当这个会话要派生任务的时候,我们只把任务放在后台是不够的,我们需要把任务独立出来,让它自成会话,自成进程组,和终端设备无关。

在这里插入图片描述

这样的任务以进程方式呈现,我们叫它守护进程!不受用户登录注销的影响,可以一直在进行运行,除非未来不想让它运行了。

那我们现在就来服务器进程守护进程化。

守护进程化有n多种方式,系统提供了一个函数。不过自己我们自己实现一个。
以后也建议用自己的。

在这里插入图片描述

一个进行想要自己变成守护进程,一定要调用setsid
谁调用这个函数,谁就自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长。

在这里插入图片描述

但是这里调用setsid不能随便调,要求调用setsid的进程不能是进程组组长。

在这里插入图片描述

#pragma once#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void deamonSelf(const char *curPath = nullptr)
{// 1.让调用进程忽略掉异常的信号// 2.如何让自己不是组长, setsid// 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件// 4.可选: 进程执行路径发送更改}

比如客户端给服务端发了一个消息,服务端收到消息然后请求完要给客户端回过去,可是正准备写回去客户端奔溃了,那么服务端此时就是像一个以及被关闭的文件描述符写入,这就如同读端关闭,写端再写没用意义,写端会收到SIGPIPE信号退出。

#pragma once#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void deamonSelf(const char *curPath = nullptr)
{// 1.让调用进程忽略掉异常的信号signal(SIGPIPE, SIG_IGN);// 2.如何让自己不是组长, setsidif (fork() > 0)//创建子进程,父进程退出exit(0);//子进程  --守护进程也叫精灵进程,本质就是孤儿进程的一种pid_t n = setsid();assert(n != 1);// 3.守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件// 4.可选: 进程执行路径发送更改}

守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件。

守护进程和显示器和键盘等已经没关系了,它就是一个独立的在后端运行,只有通过网络端口的方式进行访问。默认会打开0,1,2,可以直接close但是特别简单除暴不合理万一有些日志没写到文件中而打印到显示器但是我们关闭了那不就出问题了吗,进程之间挂掉了。因此我们选择重定向。

linux中存在一个特殊的文件,这个文件就像一个黑洞 ,默认处理方式,凡是向这个文件中写入都统统都丢弃掉。你读我也不阻塞你什么也读不到

在这里插入图片描述
在这里插入图片描述

#pragma once#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#define DEV "/dev/null"void deamonSelf(const char *curPath = nullptr)
{// 1.让调用进程忽略掉异常的信号signal(SIGPIPE, SIG_IGN);// 2.如何让自己不是组长, setsidif (fork() > 0)exit(0);pid_t n = setsid();assert(n != 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);}// 4.可选: 进程执行路径发送更改if(curPath) chdir(curPath);
}
#include"tcpServer.hpp"
#include<memory>
#include"daemon.hpp"void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}// ./tcpserver port
int main(int argc,char* argv[])
{if(argc != 2){Usage(argv[0]);exit(USAGG_ERR);}uint16_t serverport=atoi(argv[1]);unique_ptr<tcpServer> tsv(new tcpServer(serverport));tsv->initServer();//守护进程deamonSelf();tsv->start();return 0;
}

现在这个服务端进程就变成守护进程了。

在这里插入图片描述

调用setsid,自建一个会话,自己就是会话的话首进程,组长进程,以及进程组组长

在这里插入图片描述

然后客户端随意访问,服务端没有任何反应,在后端自动给我们反应

在这里插入图片描述

而且日志信息也打印到对应的文件中

在这里插入图片描述

最神奇的是,我们把xshell关掉,还可以连接到这个服务器,这就把端口暴露给外部,自己写的业务别人就可以直接进行返回了。

在这里插入图片描述

这个服务器除非自己主动退出!不然一直在后台运行。

在这里插入图片描述

7.TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

在这里插入图片描述
服务端:

服务端首先创建套接字,bind绑定ip和port,然后调用listen设置sock为监听状态,一旦调用listen服务器就由关闭状态变成监听状态就允许客户端来连接了,然后调用accept获取连接。在TCP我们有两套文件描述符,一个创建套接字返回上来的listenfd只用来获取新连接,一个accept返回上来connfd是未来IOfd用它作为IO读取。

这里有个细节,accept是获取连接,并不是创建连接,所谓获取连接前提是底层已经帮我创建好了连接,然后在应用层调用accept把连接拿上来,仅此而已。

客户端:

客户端首先创建套接字,然后调用connect发起链接请求,并且在调用connect的时候OS自动帮我们绑定ip和port。

在TCP这里我们采用链接的方案叫做三次握手

connect是发起三次握手链接请求的,而真正三次握手建立链接是双方的OS自动完成的。

accept是获取链接的,链接建立好了才能获取链接,因此accept并不参与三次握手的任何细节。
也就是说上层不调用accept,三次握手依旧能完成。

获取链接了,然后客户端和服务端调用read,write等接口进行IO通信,而TCP是可靠性的,所以在发信息后对方会给ACK确认。

TCP保证可靠性和调用read、write没有任何关系,一方发信息对方给ACK确认是双方OS去完成的。 甚至这个发信息也和write和read没关系这个后面说。

曾经建立了连接,才会有未来断开连接。断开连接在TCP这里采用的是四次挥手

四次挥手的工作也是由双方OS自动完成的,和我们没用半毛钱关系,而我们决定的是什么时候四次挥手。close是上层调用触发四次挥手。

下面再进一步感性认识三次握手,四次挥手

所谓建立链接是什么?
就如一个男生喜欢一个女生,并不是喜欢他们就在一起了,男生要想和女生在一起就必须先去尝试追求一下。因此男生首先主动发起追求(主动发起连接),他问女生:你愿意做我女朋友吗?女生回答说:好啊,什么时候开始呢? 男生说:就现在把。自此双方三次握手建立成功。

那现实中男生女生在一起了,知道各自是对方男朋友女朋友究竟是在干什么呢?
一定是记下来了一些东西,比如知道他是你的男朋友,她是你的女朋友。所以双方才知道他是我的女朋友,她是我的女朋友。

因此建立链接并不是简单的做了这个动作,它是手段,真正的目的是在双方要各自维护好链接建立好相关的属性信息 。

现在有一个男生有很多女朋友,他要有记录每一个女朋友属性信息。那怎么办呢?
他就需要对每一个女朋友对象先描述,在组织起来。弄一个链接结构管理这些女朋友们。

一个服务器可能有很多客户端发起链接,服务端也需要对这些客户端的链接先描述,在组织!对这些链接用特定的数据结构管理起来。

链接的总结:建立链接是双方OS自动完成的,建立链接过程是双方为了维护链接而创立的内核数据结构,这个内核数据结构对象是要有成本的,这个成本体现在创建的时候要花的时间和空间。

断开链接:是把曾经建立好的链接信息释放掉

断开链接为什么叫四次挥手呢?可以这样理解。男女朋友在一起最后结婚了一起生活了10年,但最终被现实打败了,男生说:我们离婚把。女生说:好啊。然后过了3秒,女生说:你跟我离婚,我也要跟你离婚。男生说:好。这种叫做协商。建立链接是一方主动,所以我们需要三次握手建立链接。断开链接是双方的事情,就必须争得双方的同意。你跟我断开链接,我也要和你断开链接,这叫做协商少了任何一方都只能叫通知。又因为TCP是保证可靠性的,我给你说的话要保证你听到了,你给我说的话要保证我听到了,所以我给你协商时你给我做应答保证我给你做协商时你听到了,反之也一样。所以需要4次,也就是四次挥手。

8.TCP和UDP 对比

  • 可靠传输 vs 不可靠传输
  • 有连接 vs 无连接
  • 字节流 vs 数据报

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/2905753.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

人工智能|推荐系统——搜索引擎广告

原文题目 Dark sides of artificial intelligence: The dangers of automated decision-making in search engine advertising(JASIST,2023) 人工智能的阴暗面:搜索引擎广告自动决策的危险 摘要 随着人工智能应用的日益广泛,搜索引擎供应商越来越多地要求广告商使用基于机…

6.RGB转YCbcr

1.方法 RGB转灰度有很多种方式   1.将RGB中任意分量拿出来当做灰度值。   2.取RGB三通道的均值来当灰度值。   3.将RGB转YUV(YCbCr)然后取Y分量作为灰度值。   其余的几种实现方式较为简单&#xff0c;这里不做介绍。重点实现RGB转YCbCr。 1.1 YUV(YCbCr)格式 Y表示亮…

深度学习中的模型蒸馏技术:实现流程、作用及实践案例

在深度学习领域&#xff0c;模型压缩与部署是一项重要的研究课题&#xff0c;而模型蒸馏便是其中一种有效的方法。 模型蒸馏&#xff08;Model Distillation&#xff09;最初由Hinton等人在2015年提出&#xff0c;其核心思想是通过知识迁移的方式&#xff0c;将一个复杂的大模型…

HTTP——Cookie

HTTP——Cookie 什么是Cookie通过Cookie访问网站 我们之前了解了HTTP协议&#xff0c;如果还有小伙伴还不清楚HTTP协议&#xff0c;可以点击这里&#xff1a; https://blog.csdn.net/qq_67693066/article/details/136895597 我们今天来稍微了解一下HTTP里面一个很小的部分&…

解决“Pycharm中Matplotlib图像不弹出独立的显示窗口”问题

matplotlib的绘图的结果默认显示在SciView窗口中, 而不是弹出独立的窗口, 这样看起来就不是很舒服&#xff0c;不习惯。 通过修改设置&#xff0c;改成独立弹出的窗口。 File—>Settings—>Tools—>Python Scientific—>Show plots in toolwindow 将√去掉即可

Github多账号共存

在开发阶段&#xff0c;如果同时拥有多个开源代码托管平台的账户&#xff0c;在代码的管理上非常麻烦。那么&#xff0c;如果同一台机器上需要配置多个账户&#xff0c;怎样才能确保不冲突&#xff0c;不同账户独立下载独立提交呢&#xff1f; 我们以两个github账号进行演示 …

密码学基础-对称密码/公钥密码/混合密码系统 详解

密码学基础-对称密码/公钥密码 加解密说明1.加密解密必要因素加密安全性说明 什么是对称密码图示说明对称密码详解什么是DES?举例说明 什么是3DES什么是AES? 公钥密码什么是RSA? 对称密钥和公钥密码优缺点对比对称密码对称密码算法总结对称密码存在的问题? 公钥密码公钥密码…

工业镜头常用参数之实效F(Fno.)和像圈

Fno. 工业镜头中常用到的参数F&#xff0c;有时候用F/#&#xff0c;Fno.来表示&#xff0c;指的是镜头通光能力的参数。它可用镜头焦距及入瞳直径来表示&#xff0c;也可通过镜头数值孔径&#xff08;NA&#xff09;和光学放大倍率&#xff08;β&#xff09;来计算。有效Fno.…

linux系统装载nginx的笔记

作为一个前端开发&#xff0c;自己部署一个前端项目是不是很正常的事情&#xff0c;所以我在这里记录一下自己在linux环境中通过nginx部署前端项目的步骤&#xff0c;方便后面查看。 步骤如下&#xff1a; 1、使用管理员身份进入命令窗口&#xff0c;如果进入时提示&#xff0…

使用苹果应用商店上架工具实现应用快速审核与发布

摘要 移动应用app上架是开发者关注的重要环节&#xff0c;但常常会面临审核不通过等问题。为帮助开发者顺利完成上架工作&#xff0c;各种辅助工具应运而生。本文探讨移动应用app上架原理、常见辅助工具功能及其作用&#xff0c;最终指出合理使用工具的重要性。 引言 移动应…

阳光倒灌高准直汽车抬头显示器HUD太阳光模拟器

阳光倒灌高准直汽车抬头显示器HUD太阳光模拟器是一种高级别的模拟设备&#xff0c;用于模拟太阳光的光谱、强度及照射角度&#xff0c;应用于太阳能电池板、光伏系统等领域的研究和测试。其参数包括光谱范围、光强度、光源、照射角度、均匀性和稳定性&#xff0c;可根据需求调整…

2024最新彩虹知识付费模板MangoA全开源包含秒杀/抽奖/社群/推送等功能

二次开发增加以下功能每日秒杀每日签到官方社群多级分销在线抽奖项目投稿 每日秒杀 每日签到 官方社群 多级分销 在线抽奖 项目投稿 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88963704 更多资源下载&#xff1a;关注我。

应急响应靶机训练-Linux1题解

前言 接上文&#xff0c;应急响应靶机训练Linux1 靶机地址&#xff1a; 应急响应靶机-Linux(1) 最近感冒了&#xff0c;就没录视频版。 题解 目标&#xff1a;3个flag以及黑客的ip地址 登陆虚拟机 密码defend flag1: su history flag{thisismybaby} flag2&#xff1a;…

【学习】软件企业何时会选择第三方软件测试机构

近年来&#xff0c;随着软件行业的迅猛发展&#xff0c;软件企业对软件测试的需求也越来越大。为了保证软件的质量和稳定性&#xff0c;许多企业选择寻找第三方软件测试机构来进行软件测试。第三方软件测试机构是独立于软开发企业的专业机构&#xff0c;主要从事软件测试和质量…

OpenGL 实现“人像背景虚化“效果

手机上的人像模式,也被人们称作“背景虚化”或 ”双摄虚化“ 模式,也称为 Bokeh 模式,能够在保持画面中指定的人或物体清晰的同时,将其他的背景模糊掉。突出画面的主体部分,主观上美感更强烈。 人像模式的一般实现原理是,利用双摄系统获取景深信息,并通过深度传感器和图…

Vivado Lab Edition

Vivado Lab Edition 是完整版 Vivado Design Suite 的独立安装版本 &#xff0c; 包含在生成比特流后对赛灵思 FPGA 进行编程和 调试所需的所有功能。通常适用于在如下实验室环境内进行编程和调试&#xff1a; 实验室环境中的机器所含磁盘空间、内存和连 接资源较少。Vivad…

Android Studio控制台输出中文乱码问题

控制台乱码现象 安卓在调试阶段&#xff0c;需要查看app运行时的输出信息、出错提示信息。 乱码&#xff0c;会极大的阻碍开发者前进的信心&#xff0c;不能及时的根据提示信息定位问题&#xff0c;因此我们需要查看没有乱码的打印信息。 解决步骤&#xff1a; step1: 找到st…

C# OpenCvSharp MatchTemplate 多目标匹配

目录 效果 项目 代码 下载 效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp; using O…

主流公链 - Monero

Monero: 加密货币的隐私标杆 1. 简介 Monero&#xff08;XMR&#xff09;&#xff0c;世界语中货币的意思&#xff0c;是一种去中心化的加密货币&#xff0c;旨在提供隐私和匿名性。与比特币等公开区块链不同&#xff0c;Monero专注于隐私保护&#xff0c;使用户的交易记录和余…