当前位置: 首页 > 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. bind
InetAddr 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. listen
int 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. socket
int 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. connect
int 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.jsqmd.com/news/18535/

相关文章:

  • Spark专题-第三部分:性能监控与实战优化(2)-分区优化 - 详解
  • 补贴防薅测试用例设计
  • 20232313 2025-2026-1 《网络与系统攻防技术》实验二实验报告 - 20232313
  • 理解C++20的革命特性——协程支持2:编写简单的协程调度器 - 实践
  • 站位4
  • 九种UML常见图 -2025.10.19
  • 阿里云 CDN部署
  • 分箱效果评估:IV值和卡方
  • 2025 年电缆桥架生产厂家最新推荐排行榜:聚焦北方 / 河北区域及瓦楞 / 防火 / 模压 / 镀锌桥架优质品牌深度解析
  • 洒水清洁,音乐相伴,洒水车声音-兰花草音乐芯片详细资料
  • 04.Python百行代码制作查询工具
  • 通过一台服务器采集所有阿里云账单费用数据
  • 2025 油烟机厂家最新推荐榜:五大实力厂商技术与服务口碑评测权威发布滑轨/易清洁/免清洗/智能油烟机厂家推荐
  • VUE---打印功能
  • 高效管理超多传感器?SHxxx 集线器实现精准切换与零混淆 告别通道混乱,内置校验
  • [ACTF2020 新生赛]Include 1 文件包含
  • 鸿蒙NEXT网络管理:从“能用”到“智能”的架构演进 - 指南
  • PostgreSQL可观测性完整方案
  • 2025 年通风天窗源头厂家最新推荐:品牌定制能力、售后体系及综合实力深度测评榜单
  • 钡铼技术全新APC系列工业边缘可视化平板电脑即将重磅发布!
  • 2025年大连甘井子区优质养老机构推荐:从社区到自然的暖心之选
  • 2025年AI营销公司推荐:广东AI营销公司/广州AI营销公司如何以模块化服务破解企业增长困局
  • 2025年越南货架厂家推荐榜:立体/高位/仓储/托盘/重型/流利式/贯通式/穿梭车/模具/货架厂家,多维度解析行业实力派
  • 2025年主轴维修厂家企业推荐: 电/高速/精密/磨床/进口磨床/加工中心电/数控机床/高速电主轴维修厂家,服务商助力制造企业降本增效
  • 2025年磨床电主轴先升级推荐榜:国产/进口/内圆/外圆/无心/平面/来图定制磨床电主轴厂家,聚焦精密制造核心
  • 在写left join的时候 是大表在左侧 还是小表在左侧(二)
  • 在写left join的时候 是大表在左侧 还是小表在左侧(一)
  • 使用POI-TL组件按模板导出word文档
  • 【IEEE出版】2025年智能控制与计算科学国际学术会议 (ICICCS 2025)
  • 2025 年地铺石厂家最新推荐榜:涵盖生态/仿石/陶瓷等品类,揭秘行业口碑优质企业18厚/火烧/庭院/陶瓷地铺石厂家推荐