当前位置: 首页 > news >正文

深入了解linux网络—— TCP网络通信(上)

前言

了解了UDP通信相关接口,现在来学习TCP通信的相关接口

服务端

无论是UDP通信,还是TCP通信;都要创建套接字、绑定端口号。

1. 初始化

创建套接字

int socket(int domain, int type, int protocol);

这里要使用TCP通信,传递的参数就应该是:AF_INETSOCKSTREAM(面向字节流)

socket(AF_INET,SOCK_STREAM,0); //AF_INET 网络通信   SOCK_STREAM  面向字节流

对于socket返回值,是一个文件描述符;和UDP使用略有差别(在accept详细介绍)

绑定端口号

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

对于bind绑定端口号,需要sockfdstruct sockaddr*类型的指针对象;(这里就直接使用封装好的InetAddr具体是实现在:lesson17/chat/inetaddr.hpp · 迟来的grown/linux

bind(_sockfd, addr.GetInetAddr(), addr.GetLen());

这里服务端,IP地址就直接绑定INADDR_ANY

监听状态

对于UDP通信,只需要创建套接字、绑定端口号就可以了;

TCP通信,除此之外还需要设置监听状态

设置监听状态要是有接口 : listen

int listen(int sockfd, int backlog);

参数:

  • sockfd:创建套接字socket返回的文件描述符。
  • backlog:表示该套接字维护的连接请求队列的最大长度,也就是:等待被接受的最大连接数

返回值:

成功返回0、失败则返回-1

所以,对于UDP通信,只需要创建套接字和绑定端口号;

TCP通信,还需要设置监听状态。

class TcpServer
{
public:TcpServer(uint16_t port) : _sockfd(-1), _port(port){}~TcpServer() {}void Init(){// 1. socket_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(Level::FATAL) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success";// 2. bindInetAddr addr(_port);int b = bind(_sockfd, addr.GetInetAddr(), addr.GetLen());if (b < 0){LOG(Level::FATAL) << "bind error";exit(2);}LOG(Level::DEBUG) << "bind success";// 3. listenint l = listen(_sockfd, 5);if (l < 0){LOG(Level::FATAL) << "listen error";exit(3);}LOG(Level::DEBUG) << "listen success";}
private:int _sockfd;uint16_t _port;
};

这样,在使用时,只需创建TcpServer对象,直接调用即可:

int main(int argc, char *argv[])
{if (argc != 2){std::cout << "usage : " << argv[0] << " port" << std::endl;exit(1);}uint16_t port = std::stoi(argv[1]);TcpServer tsvr(port);tsvr.Init();sleep(100);return 0;
}

这里,我们可以使用netstat命令查看:(ntestat -naltp)

-l: 监听状态; -t:TCP通信

在这里插入图片描述


2. 读取消息

UDP通信中,创建套接字、绑定端口号之后,就可以直接调用sendtorecvfrom进行发送和接受信息。

而在TCP中,进行读取消息之前,还需要建立连接;(服务端获取连接请求,客户端发送连接请求)。

只有建立了连接,才能进行网络通信。

获取连接

客户端获取连接请求所有到的接口:accept

在这里插入图片描述

       int accept(int sockfd, struct sockaddr *_Nullable restrict addr,socklen_t *_Nullable restrict addrlen);

参数

参数相对来说还是非常好理解的:

  • sockfd创建套接字所返回的文件描述符;
  • addr:输出型参数,获取远端的addr
  • addrlen传参时表示addr的长度,调用成功后表示所获取到远端addr的长度。

返回值:

对于accept的返回值就非常有意思了:

在这里插入图片描述

看看到,如果调用成功,返回一个文件描述符;那accept返回的文件描述符和socket返回的文件描述符有什么区别呢?

socket:创建套接字返回的文件描述符,该文件描述符只用来绑定端口号和获取连接请求。

accept:对于accept返回的文件描述符,在通信时读取使用。

简单来说就是,一个服务端可能连接多个客户端;

每一个连接都存在一个文件描述符,在服务的通过文件描述符来 接受/发送信息 给客户端。

接受/发送信息

对于TCP通信,要接受信息用的接口是read;(就是进行文件读操作的read)

而发送信息用的接口是write;(就是文件写操作的write

要读取信息,用的就是accept返回的文件描述符

    void Server(int rwfd){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << "read : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 读写Server(rwfd);}}

这里读写操作和文件读写一模一样。

简单来说:TCP通信,socket返回的文件描述符只用来绑定bind、监听listen和获取连接请求accept使用;

而进行通信使用的都是accept返回的文件描述符。

3. telnet 测试

这里只是实现了server端代码,简单测试一下;

telnet命令可以用来连接某IP地址端口号(发送连接请求)

telnet IP port

在这里插入图片描述

这里可以使用netstat -natlp查看连接情况:

在这里插入图片描述

这里是在一台服务器上做测试,可以看到两条连接。

客户端

对于客户端,首先还是要创建套接字;

还是无需显示绑定UDP通信时,是首次发送信息时绑定;那TCP呢?)

TCP通信中,则是在connect成功时自动绑定。

所以,客户端除了创建套接字之外,还需要做的就是发送连接请求;

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

对于connect的参数,还是非常容易理解的:

  • sockfd:创建套接字返回的文件描述符。
  • addr:远端的sockaddr_in字段。
  • addrlenaddr的长度

返回值:

在这里插入图片描述

客户端绑定是在connect成功时自动绑定的;

connect成功/绑定成功,返回0;否则返回-1,且错误码被设置。

读写操作

对于客户端读写操作,还是使用readwrite接口;

所用的文件描述符就是socket返回的文件描述符。

#include "log.hpp"
#include "inetaddr.hpp"
using namespace hllog;
using namespace hladdr;
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "usage : " << argv[0] << " server_ip server_port" << std::endl;exit(1);}InetAddr server(argv[1], std::stoi(argv[2]));// 1. socketint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){LOG(Level::FATAL) << "socket error";exit(1);}LOG(Level::INFO) << "socket success, sockfd : " << sockfd;// 2. connectint n = connect(sockfd, server.GetInetAddr(), server.GetLen());if (n < 0){LOG(Level::FATAL) << "connect error";exit(2);}LOG(Level::INFO) << "connect success, sockfd : " << sockfd;// 写/读while (true){std::string massage;std::cout << "Please Enter #";std::getline(std::cin, massage);int wn = write(sockfd, massage.c_str(), massage.size());if (wn < 0){LOG(Level::WARNING) << "write error";break;}// 接受char buff[256];int rn = read(sockfd, buff, sizeof(buff) - 1);if (rn < 0)continue;else if (rn == 0)break;buff[rn] = '\0';std::cout << "recive : " << buff << std::endl;}return 0;
}

多进程

对于上述实现的代码,存在一个bug:一次只能处理一个client端的请求;

这是因为在server获取到一个连接时,就会长服务式的处理这个请求(读写);只要这个连接不退出,server就无法获取新的连接请求。

这里就将上述代码修改成多进程的:

server端获取到一个连接时,就创建一个子进程,让子进程去服务;父进程继续获取请求。

问题:子进程退出时,父进程如何回收?何时回收?

  • 解决方案1:将tcpserver进程对SIGCHLD信号的处理方式设置成SIG_IGN或者自定义捕捉。
  • 解决方案2 :子进程再创建子进程(孙子进程),然后子进程退出,tcpserver回收子进程;孙子进程去服务(孤儿进程,进程推了操作系统自动回收)。

对于多进程要注意:在创建子进程后要关闭不用的文件描述符。

这里就直接实现方案二:

    void Server(int rwfd){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << "read : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 读写// Server(rwfd);// 多进程pid_t pid = fork();if (pid < 0){LOG(Level::FATAL) << "fork error";exit(1);}else if (pid == 0){close(_sockfd);if (fork() == 0)Server(rwfd);exit(0);}// 父进程waitpid(pid, nullptr, 0);}}

多线程

要实现多线程版本,创建线程去执行Server

但是Server的是void(TcpServer*, int)类型的,创建线程要执行的方法:void*(void*)类型。

并且,创建出来的线程是不知道通信要使用的文件描述符的。

  • 对于线程无法访问到通信要使用的文件描述符,这里直接在TcpServer中使用一个类,表示线程调用Server需要的数据。

    需要哪些数据呢?(fd读写使用的文件描述符、TcpServer*类型的指针对象用来调用Server方法,要知道远端通信对方的IP地址和port,也需要InetAddr类型对象。)

  • 对于线程执行方法,使用一个静态成员方法Routinue,通过传参将所需要的数据传递进去。

在创建完线程之后,设置新线程detach分离,无需手动回收

        void Server(int rwfd, InetAddr &addr){while (true){char buff[256];int rn = read(rwfd, buff, sizeof(buff) - 1);if (rn < 0){// read出错LOG(Level::ERROR) << "read error";break;}else if (rn == 0){// write端退出LOG(Level::INFO) << "writer is exit";break;}// 读取成功buff[rn] = '\0';std::cout << addr.ToString() << " : " << buff << std::endl;// 发送信息, 这里简单将信息发送回去int wn = write(rwfd, buff, rn);if (wn < 0){LOG(Level::ERROR) << "write error";break;}}}class ThreadData{public:ThreadData(int fd, TcpServer *tsvr, InetAddr addr): _fd(fd), _tsvr(tsvr), _addr(addr){}int _fd;TcpServer *_tsvr;InetAddr _addr;};static void *Routinue(void *argv){ThreadData *td = static_cast<ThreadData *>(argv);td->_tsvr->Server(td->_fd, td->_addr);return nullptr;}void Start(){while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int rwfd = accept(_sockfd, (struct sockaddr *)&peer, &len);if (rwfd < 0){LOG(Level::FATAL) << "accept error";exit(4);}LOG(Level::DEBUG) << "accept success";// 多线程pthread_t tid;ThreadData* td = new ThreadData(rwfd,this,peer);pthread_create(&tid,nullptr,Routinue,td);}}

到这里,本篇文章内容就结束了,感谢支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

http://www.dtcms.com/a/441784.html

相关文章:

  • Android Jetpack 核心组件实战:ViewModel + LiveData + DataBinding 详解
  • 商务厅网站建设意见怎么做网站注册推广
  • Fragment 崩溃恢复后出现重叠问题的复现方式
  • 设计模式(C++)详解——策略模式(2)
  • 使客户能够大规模交付生产就绪的人工智能代理
  • Layui 前端和 PHP 后端的大视频分片上传方案
  • 无状态HTTP的“记忆”方案:Spring Boot中CookieSession全栈实战
  • Java 内存模型(JMM)面试清单(含超通俗生活案例与深度理解)
  • 2015网站建设专业建网站设计公司
  • vue+springboot项目部署到服务器
  • QT肝8天17--优化用户管理
  • QT肝8天19--Windows程序部署
  • 【开题答辩过程】以《基于 Spring Boot 的宠物应急救援系统设计与实现》为例,不会开题答辩的可以进来看看
  • 成都seo网站建设沈阳网站建设推广服务
  • 网站栏目名短链接在线生成官网免费
  • Task Schemas: 基于前沿认知的复杂推理任务架构
  • 第三十七章 ESP32S3 SPI_SDCARD 实验
  • 企业营销型网站特点企业信息查询系统官网山东省
  • docker-compose 安装MySQL8.0.39
  • Go语言入门(18)-指针(上)
  • Django ORM - 聚合查询
  • 【STM32项目开源】基于STM32的智能老人拐杖
  • YOLO入门教程(番外):卷积神经网络—汇聚层
  • 网站改版一般需要多久智慧团建学生登录入口
  • Dotnet接入AI通过Response创建一个简单控制台案例
  • 【论文笔记】2025年图像处理顶会论文
  • 用 Maven 配置 Flink 从初始化到可部署的完整实践
  • 做职业规划的网站seo学院
  • 怎么建优惠券网站太原seo排名外包
  • jmeter中java.net.ConnectException: Connection refused: connect