【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)

本文主要是介绍【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

TCP服务端

创建套接字

解决绑定失败问题

填充网络信息

绑定

设置监听状态

接受请求

收取和反馈消息

完整的服务端封装代码

TCP客户端

创建套接字

填充网络信息

发起连接

发送和收取消息

客户端完整代码

 一些补充


TCP服务端

初始化服务端

创建套接字

和UDP创建套接字的方式差不多,只不过我们要实现的是TCP第二个参数选用:SOCK_STREAM

解决绑定失败问题

服务端关闭绑定失败的问题通常出现在服务端程序退出后,重新启动时,可能会因为之前的套接字仍然处于 TIME_WAIT 状态,导致无法绑定到相同的地址和端口。解决这个问题的方法通常是通过设置 SO_REUSEADDRSO_REUSEPORT 选项。

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

填充网络信息

和UDP填充本地网络信息的方式一样,还是要注意以下几点:

  • 端口号主机序列转网络序列
  • IP地址点分十进制字符串转四字节网络序列
  • 设置任意的IP地址

绑定

和UDP绑定一样,要注意结构体的强转

设置监听状态

因为TCP要建立连接,一般是由客户端发起请求,服务端等待请求的到来。所以服务端要设置监听状态

int listen(int sockfd, int backlog);

返回值

listen() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即要进行监听的套接字。

  • backlog:是一个整数,指定在内核中等待处理的连接队列的最大长度。这个参数的具体含义是系统内核在拒绝新连接之前允许处于未连接状态(SYN_RCVD)的连接数量。

    如果队列满了,新的连接会被拒绝,并且客户端可能会收到 ECONNREFUSED 错误。较大的 backlog 值可以容纳更多的等待连接的客户端,但同时也会增加系统资源的消耗。

    如果 backlog 设置为 0,表示不接受连接队列,新连接会立即被拒绝。

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

启动服务端

接受请求

accept() 函数用于接受客户端的连接请求,并创建一个新的套接字来处理该连接。这个新的套接字是与客户端建立的连接的专用套接字,通过它可以进行数据的收发。

返回值

accept() 函数的返回值是一个整数,表示新创建的套接字的文件描述符:

  • 如果成功,返回值是新创建的套接字的文件描述符。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

函数调用成功后的返回值是作为我们接下来进行读取数据的文件描述符,旧的套接字依旧作为监听套接字。 

参数

  • sockfd:是一个已经通过 socket() 函数创建并调用 bind() 函数绑定了地址的监听套接字描述符,即待处理连接的监听套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储客户端的地址信息。这个参数是一个输出参数,accept() 函数会填写客户端的地址信息到这个结构体中。如果不需要知道客户端的地址,可以将这个参数设置为 NULL

  • addrlen:是一个指向 socklen_t 类型的指针,用于指定 addr 结构体的长度。在调用 accept() 函数之前,需要将 addrlen 设置为 addr 结构体的长度。这个参数也是一个输入输出参数,accept() 函数会将实际的客户端地址长度写入到这个变量中。

收取和反馈消息

因为建立连接后得到一个文件描述符,因此我们可以用使用文件的读写方法来进行我们的收取和反馈消息的操作。


完整的服务端封装代码

 TCPserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
const static int default_backlog = 5;
class TcpServer
{
public:TcpServer(uint16_t port): _port(port), _isrunning(false){}// 初始化服务器void Init(){// 第一步:套接字创建// 得到文件描述符// 本质是文件// 第一个参数表示域// 第二个参数表示套接字类型// 第三个参数为协议 默认缺省_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){cout << "Fatal Error" << endl<< "create socket error, errno code %d ,eror string %d",errno, strerror(errno);cout << endl;exit(2);}cout << "create socket success, sockfd : " << _listensock << endl;int opt=1;setsockopt(_listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));//解决服务端关闭绑定失败问题// 第二步:填充本地网络信息并且绑定和监听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){cout << "bind Error" << "errno code : " << errno << "error string : " << strerror(errno) << endl;exit(3);}cout << "bind success" << endl;// Tcp需要建立连接,一般是由客户端发起请求 服务端一直等待连接的到来// Tcp需要监听// 设置套接字为监听状态if (listen(_listensock, default_backlog) != 0){cout << "listen Error" << "errno code: " << errno << "error string : " << strerror(errno) << endl;exit(4);}cout << "listen success " << endl;}void Sverse(int fd){char buffer[1024];while (1){// 读取数据// tcp是面向字节流和文件管道差不多// 直接使用文件的读写方法即可ssize_t n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0){// 对同一个fd进行读写buffer[n] = 0;cout << "client say#" << buffer << endl;string echo_string = "client say#";echo_string += buffer;write(fd, echo_string.c_str(), echo_string.size());}else if (n == 0) // 如果返回值为0 ,代表读到了文件结尾 (对端关闭了连接){cout << "client quit..." << endl;break;}else{cout << "read Error" << "error code : " << errno << "error string : " << strerror(errno) << endl;break;}}}void Start(){// 服务器启动了_isrunning = true;while (1){// 先获取连接// 后两个参数为输入输出型参数//等同于UDP————recvfrom// 返回值 成功了返回一个非零的新的文件描述符(网络套接字) 失败了返回-1//struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, (struct sockaddr *)&peer, &len);if (sockfd < 0){cout << "accept Error" << "error code: " << errno << "error string: " << strerror(errno) << endl;// 监听失败继续监听continue;}cout << "accept success , get a new sockfd : " << sockfd << endl;// 提供服务Sverse(sockfd);close(sockfd);}}~TcpServer(){if (_listensock > 0){close(_listensock);}}private:// 端口号uint16_t _port;int _listensock;bool _isrunning;
};

Main.cc

#include<iostream>
#include<memory>
#include"TcpServer.hpp"
using namespace std;
void Usage(const string & process)
{cout<<"Usage:"<<endl<<process <<" local_port"<<endl;
}
int main(int argc ,char * argv[])
{if(argc!=2){Usage(argv[0]);return 1;}uint16_t port = stoi(argv[1]);auto* tsvr = new TcpServer(port);tsvr->Init();tsvr->Start();delete tsvr;return 0;
}

TCP客户端

创建套接字

和服务端的一样

填充网络信息

和服务端的一样

发起连接

connect() 函数用于客户端向服务器发起连接请求。它在客户端程序中使用,用于连接到远程服务器。

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

返回值

connect() 函数的返回值是一个整数,表示执行结果的状态:

  • 如果成功,返回值为 0。
  • 如果失败,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

 参数

  • sockfd:是一个已经通过 socket() 函数创建的套接字描述符,即待连接的套接字。

  • addr:是一个指向 struct sockaddr 类型的指针,用于存储远程服务器的地址信息。通常是使用 struct sockaddr_instruct sockaddr_in6 结构体来表示 IPv4 或 IPv6 地址。这个参数包含了远程服务器的 IP 地址和端口号。

  • addrlen:是一个 socklen_t 类型的整数,表示 addr 结构体的长度。

注:这个参数调用成功操作系统会自动绑定。 

发送和收取消息

因为socket的本质也是一个文件描述符,因此我们可以像上面一样使用文件的读写方法来发送和收取消息。


客户端完整代码

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
bool visitserver(string &serverip, uint16_t serverport, int *cnt)
{// 创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cout << "client sockfd error" << endl;return false;}bool ret = true;// 客户端不用绑定 , 客户端必须要有服务端的IP和port , 需要绑定,但是不需要用户显示绑定,因为client系统会随机绑定端口// Tcp发起连接的时候,client会被操作系统自动绑定// 发起连接struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);// 字符串的点分十进制转为四字节inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server)); // 自动进行绑定// 连接失败if (n < 0){cout << "connect error" << endl;ret = false;goto END;}// 重连成功,重连次数清零*cnt = 0;// 进行通信while (1){string inbuffer;cout << "Plase Enter";getline(cin, inbuffer);if (inbuffer == "quit")break;ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());if (n > 0){char buffer[1024];ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);if (m > 0){buffer[m] = 0;cout << "get a echo message ->" << buffer << endl;}else if (m == 0){break;}else{ret = false;goto END;}}else{ret = false;goto END;}}
END:close(sockfd);return ret;
}
void Usage(const string &process)
{cout << "Usage : " << process << " server_ip server_port" << endl;
}
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);return 1;}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 定制重连int cnt = 1;while (cnt <= 5){bool result = visitserver(serverip, serverport, &cnt);if (result){cnt = 5;break;}else{sleep(1);cout << "server offline, retrying...,cout : " << cnt++ << endl;cnt++;}}// 大于重连次数if (cnt >= 5){cout << "server offline" << endl;}return 0;
}//

一些补充

uint32_t inet_addr(const char *cp);

这个函数在上篇文章中是用来将点分十进制的字符串IP地址转化为四字节的网络序列的,但是这个函数由于它的实现方法原因是不是线程安全的函数,以后我们尽量使用下面的函数。

int inet_pton(int af, const char *src, void *dst);

返回值

  • 如果转换成功,返回值为 1。
  • 如果输入的地址格式不正确,返回值为 0。
  • 如果发生错误,返回值为 -1,并且设置全局变量 errno 来指示具体的错误类型。

参数

  • af:指定地址族(Address Family),通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • src:指向以字符串形式表示的 IP 地址的指针,即点分十进制格式的 IP 地址。
  • dst:指向用于存储结果的缓冲区的指针,通常是一个 struct in_addr 结构体的指针(IPv4)或 struct in6_addr 结构体的指针(IPv6)。

今天对网络套接字的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!! 

这篇关于【Linux系统化学习】网络套接字(编写简单的TCP服务端和客户端)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/963710

相关文章

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法

《ElasticSearch+Kibana通过Docker部署到Linux服务器中操作方法》本文介绍了Elasticsearch的基本概念,包括文档和字段、索引和映射,还详细描述了如何通过Docker... 目录1、ElasticSearch概念2、ElasticSearch、Kibana和IK分词器部署

Linux流媒体服务器部署流程

《Linux流媒体服务器部署流程》文章详细介绍了流媒体服务器的部署步骤,包括更新系统、安装依赖组件、编译安装Nginx和RTMP模块、配置Nginx和FFmpeg,以及测试流媒体服务器的搭建... 目录流媒体服务器部署部署安装1.更新系统2.安装依赖组件3.解压4.编译安装(添加RTMP和openssl模块

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

redis群集简单部署过程

《redis群集简单部署过程》文章介绍了Redis,一个高性能的键值存储系统,其支持多种数据结构和命令,它还讨论了Redis的服务器端架构、数据存储和获取、协议和命令、高可用性方案、缓存机制以及监控和... 目录Redis介绍1. 基本概念2. 服务器端3. 存储和获取数据4. 协议和命令5. 高可用性6.

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav