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

日期: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(); }

};
posted @   AdviseDY  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示