网络编程套接字

网络编程套接字

  • 预备知识
    • 理解源IP地址和目的IP地址
    • 认识端口号
    • 理解 "端口号" 和 "进程ID"
    • 理解源端口号和目的端口号
    • 认识TCP协议
    • 认识UDP协议
    • 网络字节序
  • socket编程接口
    • socket 常见API
    • sockaddr结构
  • 简单的UDP网络程序
    • UDP通用服务端
      • udp服务端初始化
      • udp服务端启动
      • 服务端完整代码
    • UDP通用客户端
      • udp客户端初始化
      • udp客户端启动
      • udp客户端完整代码
    • 地址转换函数
  • 简单的TCP网络程序
    • TCP通用服务端
      • tcp服务端初始化
      • tcp服务端启动
      • tcp服务端完整代码
    • TCP通用客户端
      • tcp客户端初始化
      • tcp客户端启动
      • tcp客户端完整代码
    • 守护进程
  • TCP协议通讯流程

预备知识

理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址

将数据从主机A发送到主机B;IP唯一标识唯一的主机,主机A是数据发送端也称源IP地址,主机B是数据接受端也称目的IP地址

认识端口号

上述将数据从A主机发送到B主机并不是目的,恰恰只是手段,真正通信的是机器上的软件(进程);IP用来标识主机的唯一,所以端口就是用来标识主机上进程的唯一性

由此,IP地址+该主机上的端口号用来标识该服务器上进程的唯一性

在这里插入图片描述

所以,网络通信的本质就是进程间通信
两主机进行通信的前提是需要看到同一份资源–网络资源;通信类似IO,将数据发送出去,读取收到的数据

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
  • 一个端口号只能被一个进程占用

理解 “端口号” 和 “进程ID”

进程既然已经有pid为什么还要有port(端口号)呢?

  1. pid是系统层面的,port是网络层面的;分别设置为了解耦
  2. 客户需要每次都能够找到进程,服务器的唯一性不能进行改变,恰好ip+port能够满足
  3. 并不是所有的进程都要提供网络服务,但是所有的进程都需要pid

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”

在网络通信中,ip+port标识唯一性;两主机进行通信时,发送端不仅要发送数据,还要发送一份“多余”的数据也就是自己的ip+port,这份多余的数据便会以协议的形式进行呈现

认识TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

认识UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  • 网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

在网络通信中上面这些工作已有操作系统给解决;介绍几个接口

#include <arpa/inet.h>
//主机转网络   long long
uint32_t htonl(uint32_t hostlong);
//主机转网络   short
uint16_t htons(uint16_t hostshort);
//网络转主机   long long
uint32_t ntohl(uint32_t netlong);
//网络转主机   short
uint16_t ntohs(uint16_t netshort);

socket编程接口

socket 常见API

  1. 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
  1. 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
  1. 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
  1. 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
  1. 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议; 然而, 各种网络协议的地址格式并不相同

套接字大致分为三种:网络套接字,原始套接字,Unix域间套接字
若实现编程,则需要三种不同的接口;为了方便使用,只设计一套接口,通过不同的参数,解决所有网络或者其他场景下的通信

在这里插入图片描述

结合上面套接字的接口,当进行不同的编程时,使用不同的结构体,只需要进行类型转换;通过前两个字节便可判断是类型

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数

sockaddr 结构
在这里插入图片描述

sockaddr_in 结构
在这里插入图片描述

协议家族,指明套接字用于通信的类型
在这里插入图片描述

端口号

in_port_t sin_port;

ip地址,需要注意的是这里采取的是4字节来表示ip地址;在网络通信中的ip地址形式都是点分十进制,例如"127.0.0.1";所以需要进行类型转换,下面会详细介绍
在这里插入图片描述

简单的UDP网络程序

UDP通用服务端

udp服务端初始化

  1. 创建socket,socket的作用是用来通信的
int socket(int domain, int type, int protocol);

AF_INET协议表明此socket是网络通信
在这里插入图片描述
SOCK_DGRAM表明通信的数据形式是数据报的类型
在这里插入图片描述

需要注意的是,创建socket成功返回的是一个文件描述符,所以udp通信的本质其实就是进程间通信

在这里插入图片描述

_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
  1. 绑定port,ip;未来服务器要明确的port,不能随意改变;这里就需要上面介绍的sockaddr结构,由于是网络通信所以采用的是sockaddr_in结构;未来服务端给客户端发消息,需要将port和ip要不要发送给对方
struct sockaddr_in local;
bzero(&local, sizeof(local));

这里的协议与上面不同,这里的协议是表明填充sockaddr_in结构用于网络通信

local.sin_family = AF_INET;

在前面介绍过在网络中数据统一是大端存储,这里需要调用API对数据进行转换

local.sin_port = htons(_port);

网络中的IP地址形式是点分十进制,此结构中采用的是4字节来表示,所以需要将IP数据类型转换为4字节,然后还需要转换为大端;同理这里也是采取API

local.sin_addr.s_addr = inet_addr(_ip.c_str());

一般服务端不会指定特定IP,因为一个服务器的一个端口会接受许多不同IP客户端传来的数据;如果指定IP,会导致其余客户端的数据丢失,所以一般使用"0.0.0.0"用来接受所有数据(同一端口)

local.sin_addr.s_addr = htonl(INADDR_ANY);

以上操作只是在用户栈上进行的,操作系统并不知晓,所以需要进行bind

int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

在传入数据时,需要进行类型转换以实现统一接口

int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n == -1)
{cerr << "bind error: " << errno << " : " << strerror(errno) << endl;exit(BIND_ERR);
}

udp服务端启动

服务器的本质其实就是一个死循环
服务端在未来接受客户端传来的数据时,需要知道客户端的端口和IP地址,这些数据就是保存在sockaddr_in结构中的,接受的过程这些数据是由操作系统自动进行填写;通信是双方的,既然服务端接受到了数据,所以也需要做出回应,回应的过程操作类似,这里没有进行展示

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
void start()
{// 服务器的本质其实就是一个死循环char buffer[gnum];for (;;){// 读取数据struct sockaddr_in peer;socklen_t len = sizeof(peer); ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);// 1. 数据是什么 2. 谁发的?if (s > 0){buffer[s] = 0;string clientip = inet_ntoa(peer.sin_addr); // 1. 网络序列 2. int->点分十进制IPuint16_t clientport = ntohs(peer.sin_port);string message = buffer;cout << clientip << "[" << clientport << "]# " << message << endl;}}
}

服务端完整代码

namespace Server
{using namespace std;static const string defaultIp = "0.0.0.0"; // TODOstatic const int gnum = 1024;enum{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR};typedef function<void(string, uint16_t, string)> func_t;class udpServer{public:udpServer(const uint16_t &port, const string &ip = defaultIp): _port(port), _ip(ip), _sockfd(-1){}void initServer(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(SOCKET_ERR);}cout << "socket success: "<< " : " << _sockfd << endl;// 2. 绑定port,ip(TODO)// 未来服务器要明确的port,不能随意改变struct sockaddr_in local; // 定义了一个变量,栈,用户bzero(&local, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);                  // 你如果要给别人发消息,你的port和ip要不要发送给对方local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. string->uint32_t 2. htonl(); -> inet_addr// local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n == -1){cerr << "bind error: " << errno << " : " << strerror(errno) << endl;exit(BIND_ERR);}// UDP Server 的预备工作完成}void start(){// 服务器的本质其实就是一个死循环char buffer[gnum];for (;;){// 读取数据struct sockaddr_in peer;socklen_t len = sizeof(peer); // 必填ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);// 1. 数据是什么 2. 谁发的?if (s > 0){buffer[s] = 0;string clientip = inet_ntoa(peer.sin_addr); // 1. 网络序列 2. int->点分十进制IPuint16_t clientport = ntohs(peer.sin_port);string message = buffer;cout << clientip << "[" << clientport << "]# " << message << endl;}}}~udpServer(){}private:uint16_t _port;string _ip; // 实际上,一款网络服务器,不建议指明一个IPint _sockfd;};
}

UDP通用客户端

客户端时先向服务端发送数据,所以需要提前得知IP和端口

udp客户端初始化

与服务端不同的是,客户端不需要进行显示bind;因为服务端只有一个,而客户端却是多个,在客户端第一次向服务端发送数据时,操作系统会自动对其进行bind,填入必要的数据:端口和IP

void initClient()
{// 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(2);}cout << "socket success: "<< " : " << _sockfd << endl;
}

udp客户端启动

与服务端不同的是,客户端是先向其发送数据然后再接受其反馈(反馈过程没有展示)

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
void run()
{struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(_serverip.c_str());server.sin_port = htons(_serverport);string message;while (!_quit){cout << "Please Enter# ";cin >> message;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));}
}

udp客户端完整代码

namespace Client
{using namespace std;class udpClient{public:udpClient(const string &serverip, const uint16_t &serverport): _serverip(serverip), _serverport(serverport), _sockfd(-1), _quit(false){}void initClient(){// 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd == -1){cerr << "socket error: " << errno << " : " << strerror(errno) << endl;exit(2);}cout << "socket success: "<< " : " << _sockfd << endl;}void run(){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(_serverip.c_str());server.sin_port = htons(_serverport);string message;while (!_quit){cout << "Please Enter# ";cin >> message;sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));}}~udpClient(){}private:int _sockfd;string _serverip;uint16_t _serverport;bool _quit;};
} 

展示udp通信过程
在这里插入图片描述

地址转换函数

sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换

字符串转in_addr的函数

in_addr_t inet_addr(const char *cp);

in_addr转字符串的函数

char *inet_ntoa(struct in_addr in);

简单的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客户端初始化

这个过程与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.只有组员才能自成会话,setsidif (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给服务器; (第四次)

这个断开连接的过程, 通常称为 四次挥手

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动化运维工具——Ansible

自动化运维工具——Ansible 一、Ansible概述二、ansible 环境安装部署1.管理端安装 ansible2.ansible 目录结构3.配置主机清单4.配置密钥对验证 三、ansible 命令行模块1.command 模块2.shell 模块3.cron 模块4.user 模块5.group 模块6.copy 模块7.file 模块8.hostname 模块9&a…

自定义一个仿拼多多地址选择器

前言 做了一个仿拼多多的地址选择器&#xff0c;但是与拼多多实现方法有些出入&#xff0c;大体效果是差不多的。废话不多说&#xff0c;先上一张效果动图&#xff1a; 开始 先说说本文的一些概念。地区级别&#xff1a;就是比如省级&#xff0c;市级&#xff0c;县级&#x…

map,set的封装(基于改造红黑树)

目录 引言 1.迭代器 2.map的[]重载 3.KeyOfValue模板参数 4.整体代码展示 //改造后的红黑树代码 #include <iostream> using namespace std;enum Colour {RED 0,BLACK, };//为了实现map与set封装使用同一个模板红黑树&#xff0c;前者的value是pair&#xff0c;后者…

WebAgent-基于大型语言模型的代理程序

大型语言模型&#xff08;LLM&#xff09;可以解决多种自然语言任务&#xff0c;例如算术、常识、逻辑推理、问答、文本生成、交互式决策任务。最近&#xff0c;LLM在自主网络导航方面也取得了巨大成功&#xff0c;代理程序助HTML理解和多步推理的能力&#xff0c;通过控制计算…

Spring——更快捷的存储 / 获取Bean对象

文章目录 前言一、存储 Bean 对象类注解为什么有五个类注解使用类注解存储对象配置扫描路径(重中之重)添加注解存储 Bean 对象 方法注解配置扫描路径(重中之重)使用方法注解存储对象 二、获取 Bean 对象Autowired属性注入Setter注入构造方法注入 Resource 总结 前言 本人是一个…

【雕爷学编程】MicroPython动手做(20)——掌控板之三轴加速度6

知识点&#xff1a;什么是掌控板&#xff1f; 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片&#xff0c;支持WiFi和蓝牙双模通信&#xff0c;可作为物联网节点&#xff0c;实现物联网应用。同时掌控板上集成了OLED…

htmlCSS-----定位

目录 前言 定位 分类和取值 定位的取值 1.相对定位 2.绝对位置 元素居中操作 3.固定定位 前言 今天我们来学习html&CSS中的元素的定位&#xff0c;通过元素的定位我们可以去更好的将盒子放到我们想要的位置&#xff0c;下面就一起来看看吧&#xff01; 定位 定位posi…

rust 闭包函数

函数有自己的类型&#xff0c;可以像使用基础类型一样使用函数&#xff0c;包括将函数保存在变量中、保存在 vec 中、声明在结构体成员字段中。闭包函数也是函数&#xff0c;也有自己的类型定义。不过&#xff0c;函数实际上是指针类型&#xff0c;在 rust 所有权中属于借用的关…

Tomcat修改端口号

网上的教程都比较老&#xff0c;今天用tomcat9.0记录一下 conf文件夹下server.xml文件 刚开始改了打红叉的地方&#xff0c;发现没用&#xff0c;改了上面那行

SpringBoot百货超市商城系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SpringBoot框架开发的百货超市系统。首先&#xff0c;这是一个很适合SpringBoot初学者学习的项目&#xff0c;代…

Beyond Compare和git merge、git rebase

文章目录 各个分支线将dev1 rebase进 dev2将dev1 merge进dev2 各个分支线 将dev1 rebase进 dev2 gitTest (dev2)]$ git rebase dev1local: 是rebase的分支dev1remote&#xff1a;是当前的分支dev2base&#xff1a;两个分支的最近一个父节点 将dev1 merge进dev2 gitTest (dev…

json-server创建静态服务器2

上次写的 nodejs创建静态服务器 这次再来个v2.0 利用json-server很方便就可以实现。 vscode打开文件夹&#xff0c;文件夹所在终端&#xff1a; json-server.cmd --watch db.json 这里视频教程是没有上述命令标红的&#xff0c;但是会报错&#xff0c;具体不详&#xff0c…

uniapp小程序自定义loding,通过状态管理配置全局使用

一、在项目中创建loding组件 在uniapp的components文件夹下创建loding组件&#xff0c;如图&#xff1a; 示例代码&#xff1a; <template><view class"loginLoading"><image src"../../static/loading.gif" class"loading-img&q…

SpringBoot环境标识设置及nacos匹配配置

本地环境标识设置 本地父类maven配置 可以看到相关的分类&#xff0c;设置环境标识主要需要用到profiles; <profiles><profile><id>dev</id><properties><!-- 环境标识&#xff0c;需要与配置文件的名称相对应 --><profiles.active&…

用html+javascript打造公文一键排版系统9:主送机关排版

一、主送机关的规定 公文一般在标题和正文之间还有主送机关&#xff0c;相关规定为&#xff1a; 主送机关 编排于标题下空一行位置&#xff0c;居左顶格&#xff0c;回行时仍顶格&#xff0c;最后一个机关名称后标全角冒号。如主送机关名称过多导致公文首页不能显示正文时&…

c刷题(一)

目录 1.输出100以内3的倍数 2.将3个数从大到小输出 3.打印100~200素数 方法一 方法二 4.显示printf的返回值 最大公约数 试除法 辗转相除法 九九乘法表 求十个数的最大值 1.输出100以内3的倍数 法一&#xff1a; int n 0; while (n*3 < 100){printf("%d &q…

基于物联网、视频监控与AI视觉技术的智慧电厂项目智能化改造方案

一、项目背景 现阶段&#xff0c;电力行业很多企业都在部署摄像头对电力巡检现场状况进行远程监控&#xff0c;但是存在人工查看费时、疲劳、出现问题无法第一时间发现等管理弊端&#xff0c;而且安全事件主要依靠人工经验判断分析、管控&#xff0c;效率十分低下。 为解决上述…

基于双 STM32+FPGA 的桌面数控车床控制系统设计

桌 面数控 设 备 对 小 尺寸零件加工在成 本 、 功 耗 和 占 地 面 积等方 面有 着 巨 大 优 势 。 桌 面数控 设 备 大致 有 3 种 实 现 方 案 : 第 一种 为 微 型 机 床搭 配 传统 数控系 统 &#xff0c; 但 是 桌 面数控 设 备 对 成 本 敏感 ; 第二 种 为 基 于 PC…

使用Flutter的image_picker插件实现设备的相册的访问和拍照

文章目录 需求描述Flutter插件image_picker的介绍使用步骤1、添加依赖2、导入 例子完整的代码效果 总结 需求描述 在应用开发时&#xff0c;我们有很多场景要使用到更换图片的功能&#xff0c;即将原本的图像替换设置成其他的图像&#xff0c;从设备的相册或相机中选择图片或拍…

使用serverless实现从oss下载文件并压缩

公司之前开发一个网盘系统, 可以上传文件, 打包压缩下载文件, 但是在处理大文件的时候, 服务器遇到了性能问题, 主要是这个项目是单机部署.......(离谱), 然后带宽只有100M, 现在用户比之前多很多, 然后所有人的压缩下载请求都给到这一台服务器了, 比如多个人下载的时候带宽问…