【Linux C++】网络编程:简单的客户端与服务端

news/2025/2/6 2:43:22/文章来源:https://www.cnblogs.com/advisedy/p/18700390

日期:2025.2.4(凌晨) 2025.2.5(凌晨)

学习内容:

  • 网络编程-客户端
  • 网络编程-服务端
  • 各自的封装

个人总结:

首先这里说一声,在这之间学了个线程池的实现和进程里面信号量的实现,封装的内容,但是由于内容过多,加上学这两个东西的时候查的东西有点多,写出来好麻烦,所以欠的这两篇以后会补上的。不会鸽的。

客户端:

首先先贴上代码,然后依次说明各个部分的内容:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
using std::cout;
using std::endl;int main(int argc, char* argv[]) {if (argc != 3) {cout << "USING: ./demo1 192.168.11.132 5005" << endl;return -1;}int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("socket");return -1;}struct hostent* k_host;if ((k_host = gethostbyname(argv[1])) == 0) {cout << "gethostbyname failed" << endl;close(sock_fd);return -1;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(atoi(argv[2]));memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");close(sock_fd);return -1;}char buf[1024];for (int i = 0; i < 3; i++) {int i_res;memset(buf, 0, sizeof buf);sprintf(buf, "%d", i);if ((i_res = send(sock_fd, buf, strlen(buf), 0)) <= 0) {perror("send");break;}cout << "send: " << buf << endl;memset(buf, 0, sizeof buf);if ((i_res = recv(sock_fd, buf, sizeof buf, 0)) <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;sleep(1);}close(sock_fd);return 0;
}

第一部分:

int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("socket");return -1;}

这一部分是设置一个套接字的描述符。

我们先明确一个大的方向,就是在网络连接中,我们与服务器之间的连接是通过套接字连接的,服务器有一个套接字,我们这里也需要有一个套接字,套接字和套接字连接上就可以进行连接。所以这一步就是用来进行创建套接字,为后面连接做准备。

这里socket的参数基本都是固定的,因为我们大多数都是使用ipv4协议,tcp协议,如果是要做音视频通话之类的,我们一般就是用udp协议。第二个参数就写SOCK_DGRAM

前两个参数分别代表着ipv4和tcp,第三个参数填0是交给编译器自动填值,我们可以默认填0就好了。

第二部分:

 	struct hostent* k_host;if ((k_host = gethostbyname(argv[1])) == 0) {cout << "gethostbyname failed" << endl;close(sock_fd);return -1;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(atoi(argv[2]));memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);

看到了serv_addr没有,这个是我们第二部分的主角。

在网络连接中,只有接口是不够的,作为客户端的我们,还需要知道服务端的ip地址,网络协议(其实绝大部分都是ipv4),端口。

一开始有一个k_host,这个是用来为后面设置serv_addr的ip地址做准备的。

中间我们用到一个叫gethostbyname的函数,这个函数是将域名换成ip地址的。

实际上gethostbyname 既可以处理域名,也可以处理点分十进制的 IP 地址字符串。

我们会在运行的时候输入命令

./demo1 192.168.11.132 5005

这里的ip地址和端口都是我预先设置好的,端口这个我用的虚拟机是ubuntu,百度搜一下ubuntu如何开启防火墙的端口就好了。自己设置一个端口就可以。

(k_host = gethostbyname(argv[1]))

这个k_host是个结构体,结构体名字是hostent,里面有很多的内容,我们了解里面有个成员是h_addr_list就好了,这个成员里面存储的是主机的 IP 地址,并且这些 IP 地址是以网络字节序表示的。

插入小知识:网络字节序

计算机中存储数据有大端序和小端序两种方式。为保证网络中数据的一致性,出现了网络字节序,其实就是大端序。
比如,一个整数 0x12345678,在大端序中存储为 12 34 56 78,而在小端序中存储为 78 56 34 12 。
在网络编程中,规定网络传输的数据采用大端序,因此处理数据时要注意转换字节序。
例如,我们可以使用 htons 函数将主机字节序的短整数转换为网络字节序。

再来看我们的主角serv_addr:

结构体类型是sockaddr_in,这里先不详说,等到下个部分会展开说的。

我们就先知道,与服务端连接的时候,要设置以下三个:

serv_addr.sin_family = AF_INET;//这个AF_INET就是代表ipv4的意思,这个地方基本是固定不动的。serv_addr.sin_port = htons(atoi(argv[2]));//argv[2]是我们输入的时候的端口号,先用atoi函数(作用是将一个字符串转换成int),然后再用htons函数(作用是将主机字节序转化成网络字节序的short类型)。memcpy(&serv_addr.sin_addr, k_host->h_addr_list[0], k_host->h_length);//这个是设置我们所要连接的服务端的ip地址

第三部分:

	if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");close(sock_fd);return -1;}

第三部分就是和服务端取得连接,这里我们抽象的理解,利用connect函数连接了之后,我们以后就可以用这个套接字描述符来抽象的面对服务端,也就是说我们可以把这个描述符就当做服务端。

这里有个地方,是关于(struct sockaddr*)&serv_addr

为什么这个地方要从sockaddr_in类型强制转换成 sockaddr类型呢?

这是因为在网络编程的底层接口设计中,sockaddr 是一个更通用的结构体类型。虽然我们实际使用的是 sockaddr_in 来专门处理 IPv4 的情况,但在一些系统函数(比如 connect )的参数要求中,需要的是 sockaddr 类型。

这样做可以保持接口的通用性和兼容性,使得函数能够处理不同类型的地址(比如 IPv6 或者其他网络协议的地址)。虽然我们这里明确是 IPv4 ,但为了满足函数的要求,还是需要进行这样的强制类型转换。另外,这种强制类型转换是被支持的,不会出错。

那为什么不一开始使用sockaddr呢?

这是因为 sockaddr 结构体比较通用但不够具体呀。它的成员定义比较简单,不能很好地满足我们在处理 IPv4 时的具体需求。

sockaddr_in 结构体专门为 IPv4 进行了设计,比如有专门的 sin_port 来表示端口,sin_addr 来表示 IP 地址。

虽然最终在某些函数调用时需要强制转换为 sockaddr ,但在前期的设置和操作中,使用 sockaddr_in 能让我们的代码更易读、更易编写和理解。

第四部分:

	char buf[1024];for (int i = 0; i < 3; i++) {int i_res;memset(buf, 0, sizeof buf);sprintf(buf, "%d", i);if ((i_res = send(sock_fd, buf, strlen(buf), 0)) <= 0) {perror("send");break;}cout << "send: " << buf << endl;memset(buf, 0, sizeof buf);if ((i_res = recv(sock_fd, buf, sizeof buf, 0)) <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;sleep(1);}

这一部分就是数据的发送和接收。

先看send函数的部分,这里就是我们发送数据的内容。

其实我们会发现,这和我们在操作文件的时候有很多相似的地方,其实他们本质都差不多,都是利用描述符来进行输入输出等操作。

这里没有太多好讲的,参数部分看了函数也就大概知道什么意思了,就不多讲了。

这里稍微说一下:

send 函数返回实际发送的字节数,recv 函数返回实际接收的字节数。

// send返回值说明:
// >0: 成功发送的字节数
// 0: 可能发生在非阻塞socket且发送缓冲区满时
// -1: 错误(需检查errno)// recv返回值说明:
// >0: 接收到的字节数
// 0: 连接已正常关闭(FIN接收)
// -1: 错误(需检查errno)

send函数的第四个参数是flags,用于指定发送数据时的一些特殊行为。我们一般默认填0,代表没有什么特殊操作。了解就好。

第五部分:

其实很简单,就是关闭我们的描述符。

close(sock_fd);

总结:

我们稍微的总结一下大概的流程:

首先我们用socket函数来创建一个套接字描述符int sock_fd = socket(AF_INET, SOCK_STREAM, 0);,然后接下里的任务就是用connect函数将fd和serv_addr绑定在一起。

也就是说我们要先设置好serv_addr(sockaddr_in类型),然后用connect函数。

设置serv_addr,需要准备好ip地址,端口还有协议。

ip地址我们要用到hontent类型,将其用gethostbyname函数赋值。

端口就用htons(atoi(val))。

协议我们已知ipv4,也就是AF_INET。

然后用connect函数绑定好了,我们就可以使用send和recv函数。

最后不要忘记close就好了。

服务端:

首先先贴上代码:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
using std::cout;
using std::endl;int main(int argc, char* argv[]) {if (argc != 2) {cout << "USING: ./demo2 5005" << endl;return -1;}int listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (listen_fd == -1) {perror("socket");return -1;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(atoi(argv[1]));if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("bind");close(listen_fd);return -1;}if (listen(listen_fd, 5) != 0) {perror("listen");close(listen_fd);return -1;}int client_fd = accept(listen_fd, 0, 0);if (client_fd == -1) {perror("accept");close(listen_fd);return -1;}char buf[1024];while (true) {int i_res;memset(buf, 0, sizeof(buf));i_res = recv(client_fd, buf, sizeof(buf), 0);if (i_res == 0) {cout << "END" << endl;break;}if (i_res <= 0) {perror("recv");break;}cout << "recv: " << buf << endl;memset(buf, 0, sizeof(buf));strcpy(buf, "ok");if ((i_res = send(client_fd, buf, strlen(buf), 0)) <= 0) { perror("send");break;}cout << "send: " << buf << endl;}close(client_fd);close(listen_fd);return 0;
}

我们大体发现,其实和客户端的内容有很多地方是相通的,所以我们这里着重介绍一下不同的地方就好了。

两个套接字描述符:listenfd和clientfd

先说这两个套接字描述符的作用:

在服务器启动时,需要将服务器的地址(IP 地址和端口号)绑定到一个套接字描述符上,这个描述符就是 listenfd。这个套接字是用于监听客户端的连接的请求的。当有客户端需要连接的时候,我们会用clientfd来建立一个描述符,这个描述符会用来帮助进行客户端和服务端的数据交流。

用这两个套接字描述符来分开处理连接和数据交流的功能,这样,服务器可以同时处理多个客户端的连接请求,提高并发处理能力。当有新的客户端连接请求到来时,服务器可以继续使用 listenfd 监听其他连接请求,而使用新的 clientfd 与新的客户端进行通信。还有一个原因是代码阅读起来比较方便。

serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

这里有一个小细节s_addr,这个其实是一个宏,对应的是h_addr_list[0]。

还有的是INADDR_ANY,这个是指示服务器监听所有可用的网络接口上的连接。

用htonl函数,因为地址的数字,用long而不是short。所以不用htons

bind函数

这里的bind函数不是std::bind函数,而是socket里面用于将监听描述符绑定服务器的地址和端口号的。与客户端里的connect函数类似。

listen(listenfd,nums)

listen函数是用来限制监听描述符的消息队列的长度,以及把listenfd从主动状态变成被动状态(官方话术)。

其实大概的意思我们可以拿客户端和服务端举例,例如客户端一般是主动的那一方,会发送消息,请求什么之类的,而服务端是等待客户端,然后根据其做出回应,像是被动状态。而listen函数就是把描述符变成被动状态,可以用来监听。

accept函数

accept(listenfd, (struct sockaddr *)&client_addr, &client_addr_len);

第一个参数是监听描述符,第二个是client_addr的强制转换,第三个是client_addr_len,这个变量我们最开始会初始化是sizeof(sock_addr_in)。

但是accept函数调用之后会实际存储的客户端地址信息的长度修改在client_addr_len。

当然在一些情况,后面两个参数我们可以填0。

填0代表我们并不关心客户端的ip地址和信息的长度大小。

accept函数的返回值是clientfd客户端套接字描述符。

other:

send函数和recv函数就和客户端的差不多,只不过要注意我们要发送和接受的描述符都是客户端套接字描述符。

最后要记得close,而且两个套接字描述符我们都要关闭。

总结:

所以我们首先先用socket函数创建一个listenfd,然后我们的目的是用bind函数将listenfd和服务端的地址等信息绑定起来。

所以我们要用sock_addr_in来设置,设置好了之后用bind函数绑定。

然后再用listen函数将listenfd变成被动状态,再用accpet函数创建出来clientfd。

之后就可以正常的发送接收消息,最后close即可。

封装

看上去很复杂,但是我们将他们封装之后,就会很简洁:

//客户端的main函数代码
int main(int argc, char* argv[]) {if (argc != 3) {cout << "USING: ./demo1 192.168.11.132 5005" << endl;return -1;}TCPclient client;client.connect(argv[1], atoi(argv[2]));std::string buf;for (int i = 0; i < 6; i++) {int i_res;buf = std::to_string(i);client.send(buf);cout << "send: " << buf << endl;client.recv(buf, 1024);cout << "recv: " << buf << endl;sleep(1);}return 0;
}//服务端的main函数代码
int main(int argc, char* argv[]) {if (argc != 2) {cout << "USING: ./demo2 5005" << endl;return -1;}TCPserver server;server.Init(atoi(argv[1]), 5);server.Accept();std::string buf;while (true) {int i_res;i_res = server.Recv(buf, 1024);if (i_res == false) break;cout << "recv: " << buf << endl;i_res = server.Send("ok");cout << "send: ok" << endl;}return 0;
}

服务端:

class TCPserver {
private:struct sockaddr_in serv_addr;public:int _listen_fd;int _client_fd;std::string _client_ip;unsigned short _port;TCPserver() :_listen_fd(-1), _client_fd(-1) {}bool Init(const unsigned short port, int max_size) {if (_listen_fd != -1) return false;_listen_fd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_fd == -1) return false;_port = port;struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(_listen_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {close(_listen_fd);_listen_fd = -1;return false;}if (listen(_listen_fd, max_size) == -1) {close(_listen_fd);_listen_fd = -1;return false;}return true;}bool Accept() {struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);_client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);if (_client_fd == -1) return false;_client_ip = inet_ntoa(client_addr.sin_addr);return true;}bool Send(const std::string& buf) {if (_client_fd == -1) return false;int i_res;if ((i_res = send(_client_fd, buf.c_str(), buf.size(), 0)) <= 0) return false;return true;}bool Recv(std::string& buf, const size_t max_len) {if (_client_fd == -1) return false;buf.clear();buf.resize(max_len);int read_len;if ((read_len = recv(_client_fd, &buf[0], buf.size(), 0)) <= 0) {buf.clear();return false;}buf.resize(read_len);return true;}bool CloseListen() {if (_listen_fd == -1) return false;close(_listen_fd);_listen_fd = -1;return true;}bool CloseClient() {if (_client_fd == -1) return false;close(_client_fd);_client_fd = -1;return true;}~TCPserver() {CloseListen();CloseClient();}
};

客户端:

class TCPclient {
public:int _client_fd;std::string _ip;unsigned short _port;TCPclient() :_client_fd(-1) {}bool connect(const std::string& ip, const unsigned short port) {if (_client_fd != -1) return false;int sock_fd = socket(AF_INET, SOCK_STREAM, 0);if (sock_fd == -1) {perror("connect");return false;}_client_fd = sock_fd, _ip = ip, _port = port;struct hostent* h = gethostbyname(ip.c_str());if (h == 0) {perror("connect: gethostbyname failed");::close(_client_fd);_client_fd = -1;return false;}struct sockaddr_in serv_addr;memset(&serv_addr, 0, sizeof serv_addr);serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);memcpy(&serv_addr.sin_addr, h->h_addr, h->h_length);if (::connect(_client_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {perror("connect");::close(_client_fd);_client_fd = -1;return false;}return true;}bool send(const std::string& buf) {if (_client_fd == -1) return false;int i_res;if ((i_res = ::send(_client_fd, buf.c_str(), buf.size(), 0)) <= 0) return false;return true;}bool recv(std::string& buf, const size_t max_len) {if (_client_fd == -1) return false;buf.clear();buf.resize(max_len);int read_len;if ((read_len = ::recv(_client_fd, &buf[0], buf.size(), 0)) <= 0) {buf.clear();return false;}buf.resize(read_len);return true;}bool close() {if (_client_fd == -1) return false;::close(_client_fd);_client_fd = -1;return true;}~TCPclient() { close(); }};

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

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

相关文章

从源码分析arm64中断与GIC

本文以树莓派4b(armv8)来实现,4b支持两种传统的中断控制器 gic-400 但是使用的qemu和实际的板子都是默认支持gic-400的,所以主要是借助gic-400实现中断的功能异常处理 相关寄存器PSTATE 就是cpu状态DAIF 调试异常 SError(系统异常) IRQ(中断) FIQ(快速中断)esr_elx 用来保存返…

《gm/ID设计法基本介绍》翻译

最近流片很累很焦虑,放松心情找篇讲\(g_m/I_D\)设计法的文档翻译一下: 《A Basic Introduction to the gm/ID-Based Design Methodology》 1. 摘要 该文章向读者介绍了基于\(g_m/I_D\)的设计方法学,用于帮助CMOS模拟电路设计者将晶体管物理参数与小信号模型联系起来,文章的…

个人英语学习笔记基于B站英语的平行世界语法课程

导读 语言学习没有捷径,只要听说读写这四大行长期日复一日的练习就行了,兴趣是最重要的,兴趣就是高效学习的基础和长期坚持下去的动力。 0基础开始痛苦学习大半年英语,没兴趣的结果就是词汇量是上去了,但是英语的听说读写水平还不如学了一年的日语。😅 该笔记基于此课程…

PostgreSQL:数据库迁移与版本控制

title: PostgreSQL:数据库迁移与版本控制 date: 2025/2/6 updated: 2025/2/6 author: cmdragon excerpt: 在现代软件开发中,数据库作为应用程序的核心组件之一,数据的结构和内容必须能够随着业务需求的变化而调整。因此,数据库迁移和版本控制成为了确保数据一致性、完整性…

Servlet基础

什么是Servlet、Servlet的架构、Servlet任务、Servlet的基本使用、Servlet的生命周期、Servlet API中主要接口及实现类、Servlet的部署(注册与映射)、缺省Servlet与启动时加载配置、ServletConfig与ServletContext、request和response什么是Servlet基础 Java Servlet 是运行在…

GNURadio模块学习——Source and Sink类

介绍GNU Radio中常见的 Source 与 Sink 模块,包括流程图端口、音频输入输出、虚拟连接、文件读写、ZMQ跨流程图通信,以及随机信号源、固定信号源、噪声源等常见信号源和时域、频域、星座图等信号展示工具。Source and Sink Pad(流程图端口) 当该流程图是hierarchical block…

【C++】gflag使用指南

一、什么是gflags? gflags 是一个用于定义命令行参数的 C++ 库,它由 Google 开发并开源。通过 gflags,你可以轻松地在你的程序中添加各种类型的命令行选项,包括整数、布尔值、字符串等,并且可以为这些选项设置默认值。此外,gflags 还提供了强大的帮助信息生成功能,使得用…

【C++】Google benchmark理解与应用

一、介绍 Google Benchmark 是一个用于 C++ 的微基准测试库。它旨在帮助开发者编写出更高效、更具表现力的基准测试代码。通过使用 Google Benchmark,可以方便地测量函数或代码片段的性能,并且能够生成详细的报告。 二、安装与配置 2.1 安装 在Ubuntu环境中安装Google Benchm…

LRU浅析

LRU算法LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使…

20250205 省选模拟赛 T3

20250205 省选模拟赛 T3 Description 设计一个 \(n\times n\) 的 01 矩阵,使得从 \((1,1)\) 走到 \((n,n)\) 且只能向右或下走且只经过为 \(1\) 的格子的方案数为 \(X\)。 \(n \leq 24\) 时得满分。\(X \leq 10^9\)。 Solution 基于 \(2\) 进制的构造方法我们称从左上到右下的…

Automa:自动化浏览器工作流

🏷️仓库名称:AutomaApp/automa 🌟截止发稿星数: 14340 (今日新增:33) 🇨🇳仓库语言: Vue 🤝仓库开源协议:Other 🔗仓库地址:https://github.com/AutomaApp/automa引言 Automa是一个浏览器扩展,允许用户通过连接模块来自动化浏览器任务。它消除了重复性任务的需…

本地部署DeepSeek教程

本地部署DeepSeek教程 步骤 本地部署DeepSeek教程步骤 1 安装Ollama 2 下载DeepSeek模型 3 可视化图文交互界面Chatbox(可选)1 安装Ollama 访问Ollama官网下载Ollama,默认安装即可。安装完成后打开终端(我这里是windows系统),输入: ollama help即可查看ollama选项,且可…