【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使用fdisk进行磁盘的相关操作

《Linux使用fdisk进行磁盘的相关操作》fdisk命令是Linux中用于管理磁盘分区的强大文本实用程序,这篇文章主要为大家详细介绍了如何使用fdisk进行磁盘的相关操作,需要的可以了解下... 目录简介基本语法示例用法列出所有分区查看指定磁盘的区分管理指定的磁盘进入交互式模式创建一个新的分区删除一个存

利用Python编写一个简单的聊天机器人

《利用Python编写一个简单的聊天机器人》这篇文章主要为大家详细介绍了如何利用Python编写一个简单的聊天机器人,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 使用 python 编写一个简单的聊天机器人可以从最基础的逻辑开始,然后逐步加入更复杂的功能。这里我们将先实现一个简单的

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

Python手搓邮件发送客户端

《Python手搓邮件发送客户端》这篇文章主要为大家详细介绍了如何使用Python手搓邮件发送客户端,支持发送邮件,附件,定时发送以及个性化邮件正文,感兴趣的可以了解下... 目录1. 简介2.主要功能2.1.邮件发送功能2.2.个性签名功能2.3.定时发送功能2. 4.附件管理2.5.配置加载功能2.6.

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

Linux Mint Xia 22.1重磅发布: 重要更新一览

《LinuxMintXia22.1重磅发布:重要更新一览》Beta版LinuxMint“Xia”22.1发布,新版本基于Ubuntu24.04,内核版本为Linux6.8,这... linux Mint 22.1「Xia」正式发布啦!这次更新带来了诸多优化和改进,进一步巩固了 Mint 在 Linux 桌面

LinuxMint怎么安装? Linux Mint22下载安装图文教程

《LinuxMint怎么安装?LinuxMint22下载安装图文教程》LinuxMint22发布以后,有很多新功能,很多朋友想要下载并安装,该怎么操作呢?下面我们就来看看详细安装指南... linux Mint 是一款基于 Ubuntu 的流行发行版,凭借其现代、精致、易于使用的特性,深受小伙伴们所喜爱。对

使用IntelliJ IDEA创建简单的Java Web项目完整步骤

《使用IntelliJIDEA创建简单的JavaWeb项目完整步骤》:本文主要介绍如何使用IntelliJIDEA创建一个简单的JavaWeb项目,实现登录、注册和查看用户列表功能,使用Se... 目录前置准备项目功能实现步骤1. 创建项目2. 配置 Tomcat3. 项目文件结构4. 创建数据库和表5.

什么是 Linux Mint? 适合初学者体验的桌面操作系统

《什么是LinuxMint?适合初学者体验的桌面操作系统》今天带你全面了解LinuxMint,包括它的历史、功能、版本以及独特亮点,话不多说,马上开始吧... linux Mint 是一款基于 Ubuntu 和 Debian 的知名发行版,它的用户体验非常友好,深受广大 Linux 爱好者和日常用户的青睐,

使用PyQt5编写一个简单的取色器

《使用PyQt5编写一个简单的取色器》:本文主要介绍PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16进制颜色编码,一款跟随鼠标刷新图像的RGB和16... 目录取色器1取色器2PyQt5搭建的一个取色器,一共写了两款应用,一款使用快捷键捕获鼠标附近图像的RGB和16