26. Linux C/C++ TCP/UDP网络编程与高阶技巧

1.前言

上一章我们已经基本学会了Linux系统的基本使用方法,说白了就是需要记忆大量的命令,但也要讲究技巧。

而Linux系统应用范围最广的地方其实是服务器,因为Linux系统可以没有图形化界面,内核可以相当小,性能也非常好,最重要的是开源免费。

这些优势就导致,当今世界上绝大多数服务器,都是在Linux系统上开发的,而开发一个高性能的服务器,C/C++语言自然也就是不二选择。

也因此C/C++语言的一个大的就业方向就是Linux服务器开发。

2.建立项目

首先还是建立项目吧,想要高效率的开发软件,IDE肯定必不可少,所以还是采用VS开发。

还是Linux控制台项目:

image-20240103183421898

注意要打开ubuntu终端,开启ssh服务:

sudo service ssh start

这样才能正常写代码,否则VS里面会报一大堆错误,找不到文件什么的。

然后运行一下,如果能正常编译运行就没问题:

image-20240103183943069

如果出现VS报错,找不到文件什么的,这很正常,毕竟的通过网络传输实现的跨平台编程,不确定因素有很多。

所以一定要确保linux平台ssh服务是开启状态,并且VS可以正常连接上去。

可以先试着编译以下,只要编译没错误,就说明没问题,右键VS里面任意区域,选择重新扫描文件、项目,一般就能恢复识别。

3.Tcp编程

3.1 服务器

首先我们还是来写一个Tcp协议的聊天软件,还记得在Windows系统上如何写Tcp吗?

不记得了一定要返回重新看一看,因为网络编程的逻辑都差不多,所以我这里不会再重复赘余的讲解。

过程大致相同,但个人觉得,Linux开发起来更加简洁,首先就是在Linux系统上开发服务器,不需要加载动态库什么的,包含头文件就可以直接使用。

其次就是,Linux编程不像Windows那样,定义很多自定义类型。

首先看一下我们需要的头文件:

#include <cstdio> //等价于stdio.h,这是C++的命名规则
#include<sys/socket.h> //众多我们要使用的网络函数,比如bind,socket,connect等等
#include<arpa/inet.h> //一些我们会使用到的网络变量,比如地址sockaddr_in 这个结构体就在该头文件中
#include <unistd.h> //包含众多常见Linux API,比如本文要使用的close函数,因为socket在linxu里面也是一个文件,所以关闭文件统一使用close函数
#include<string.h> //包含一些基本的字符串操作函数,比如strcmp,比较字符串

有了上面的头文件,我们就可以来写一个服务器函数:

void Server() {
	int sockListen = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	int ret = bind(sockListen, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		printf("绑定失败!");
		return;
	}

	listen(sockListen, 5);

	sockaddr_in cliAddr;
	socklen_t len = sizeof(cliAddr);
	int clisock = accept(sockListen, (sockaddr*)&cliAddr, &len);
	if (clisock == -1) {
		printf("接收客户端错误");
		return;
	}

	printf("客户端已经连接!\n");

	while (1) {
		char buf[0xFF]{};
		size_t len=recv(clisock, buf, 0xFF, 0);
		if (len <= 0) {
			printf("客户端已经断开连接!\n");
			break;
		}
		send(clisock, buf, len, 0);
	}
	close(sockListen);
	close(clisock);
}

步骤基本与windows网络编程中相同,不同之处就是,不用自己加载网络库,关闭套接字不再使用closesocket,而是close函数。

注意,socket返回的是一个int值,因为linux遵循万物皆文件的思想,所以都是直接使用数字代表一个文件。

包括linux中的普通文件操作API等等都是如此,但由于C/C++语言是跨平台的,所以你完全可以继续使用以前的C/C++方式操作Linux系统中的文件。

还有一些新的类型,比如socklen_t 等等,还是老方法,在VS里面直接右键看其真实类型是什么即可,之所以要使用这个变量类型,单纯是因为后面的函数参数是这个类型。

上面的代码就实现了一个回声服务器的基本功能

所谓回声服务器,就是你发给服务器什么内容,服务器都会原封不动的发送回来

这就不需要你手动去服务器程序来给客户端发送消息了,这适用于测试网络是否能够正常通信。

3.2 客户端

上面写了一个服务器的函数,现在再来写一个客户端的函数,相比较而言会更加简单:

void Client() {
	int sockCli = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr_in addr{};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int ret = connect(sockCli, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		printf("连接服务器失败!");
		return;
	}

	char buf[0xFF];

	while (1) {
		printf("请输入信息:");
		scanf("%s",buf);
		send(sockCli, buf, strlen(buf), 0);
		memset(buf, 0, 0xFF);
		ssize_t len=recv(sockCli, buf, 0xFF, 0);
		printf("服务器:%d|%s\n", len,buf);
	}

	close(sockCli);
}

步骤基本都差不多,完成了Windows网络编程学习的,这种应该随便自己看看都能看懂,我也就不多说了。

3.3 完整代码

上面写了两个函数,而没有写在两个项目里面,就是为了方便看而已:

#include <cstdio>
#include<sys/socket.h>
#include<arpa/inet.h>
#include <unistd.h>
#include<string.h>
void Server() {
	int sockListen = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	int ret = bind(sockListen, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		printf("绑定失败!");
		return;
	}

	listen(sockListen, 5);

	sockaddr_in cliAddr;
	socklen_t len = sizeof(cliAddr);
	int clisock = accept(sockListen, (sockaddr*)&cliAddr, &len);
	if (clisock == -1) {
		printf("接收客户端错误");
		return;
	}

	printf("客户端已经连接!\n");

	while (1) {
		char buf[0xFF]{};
		size_t len=recv(clisock, buf, 0xFF, 0);
		if (len <= 0) {
			printf("客户端已经断开连接!\n");
			break;
		}
		send(clisock, buf, len, 0);
	}
	close(sockListen);
	close(clisock);
}

void Client() {
	int sockCli = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr_in addr{};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int ret = connect(sockCli, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		printf("连接服务器失败!");
		return;
	}

	char buf[0xFF];

	while (1) {
		printf("请输入信息:");
		scanf("%s",buf);
		send(sockCli, buf, strlen(buf), 0);
		memset(buf, 0, 0xFF);
		ssize_t len=recv(sockCli, buf, 0xFF, 0);
		printf("服务器:%d|%s\n", len,buf);
	}

	close(sockCli);
}

int main(int argc,char *argv[])
{
	if (argc == 1) {
		printf("请输入参数!");
		return -1;
	}

	if (strcmp("s", argv[1]) == 0) {
		printf("服务器开启!\n");
		Server();
	}
	else if(strcmp("c", argv[1]) == 0) {
		printf("客户端开启!\n");
		Client();
	}
	else {
		printf("无效参数!\n");
	}

}

通过main函数传入的参数,来判断使用哪一个函数,这样就实现了一个程序既可以当服务器,又可以当客户端!

最后点击生成即可。

3.4 运行测试

注意,linux系统是可以同时开启多个终端的,比如我这里就开启了两个终端,都来到了对应的程序生成目录中。

我们先传入参数"s"开启服务器,再传入参数"c"在另一个终端开启客户端:

image-20240103184323355

此时就能完美运行,客户端发送的任何消息都会立马收到服务器的回应。

4.UDP编程

学会了TCP编程,UDP就会简单很多,因为它还省略很多步骤,只需要换一些参数即可。

这里直接上代码:

#include <cstdio>
#include<sys/socket.h>
#include<arpa/inet.h>
#include <unistd.h>
#include<string.h>
void Server() {
	int sockListen = socket(AF_INET, SOCK_DGRAM, 0);
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	int ret = bind(sockListen, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		printf("绑定失败!");
		return;
	}

	sockaddr_in cliAddr;
	socklen_t aLen= sizeof(cliAddr);
	while (1) {
		char buf[0xFF]{};
		size_t len=recvfrom(sockListen, buf, 0xFF, 0,(sockaddr*)&cliAddr,&aLen);
		if (len <= 0) {
			continue;
		}
		sendto(sockListen, buf, len, 0, (sockaddr*)&cliAddr, aLen);
	}
	close(sockListen);
}

void Client() {
	int sockCli = socket(AF_INET, SOCK_DGRAM, 0);
	sockaddr_in addr{};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(5000);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");

	char buf[0xFF];
	socklen_t alen = sizeof(addr);