Windows/Linux TCP Socket网络编程简介及测试代码

2023-05-16

典型的网络应用是由一对程序(即客户程序和服务器程序)组成的,它们位于两个不同的端系统中。当运行这两个程序时,创建了一个客户进程和一个服务器进程,同时它们通过从套接字(socket)读出和写入数据在彼此之间进行通信。开发者创建一个网络应用时,其主要任务就是编写客户程序和服务器程序的代码。

网络应用程序有两类。一类是由协议标准(如一个RFC或某种其它标准文档)中所定义的操作的实现,这样的应用程序有时称为”开放”的,因为定义其操作的这些规则为人们所共知。对于这样的实现,客户程序和服务器程序必须遵守由该RFC所规定的规则。另一类网络应用程序是专用的网络应用程序。在这种情况下,由客户和服务器程序应用的应用层协议没有公开发布在某RFC中或其它地方。某单独的开发者(或开发团队)产生了客户和服务器程序,并且该开发者用他的代码完全控制该代码的功能。但是因为这些代码并没有实现一个开放的协议,其它独立的开发者将不能开发出和该应用程序交互的代码。

RFC(Request for Comments):请求意见稿,是由互联网工程任务组(IETF)发布的一系列备忘录。文件收集了有关互联网相关信息,以及UNIX和互联网社群的软件文件,以编号排定。目前RFC文件是由互联网协会(ISOC)赞助发行。基本的互联网通信协议都有在RFC文件内详细说明。RFC已经成为IETF、Internet Architecture Board(IAB)还有其他一些主要的公共网络研究社区的正式出版物发布途径。RFC文件只有新增,不会有取消或中途停止发行的情形。但是对于同一主题而言,新的RFC文件可以声明取代旧的RFC文件。

TCP网络编程有两种模式:一种是服务器模式,另一种是客户端模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。

TCP是面向连接的,并且为两个端系统之间的数据流动提供可靠的字节流通道。UDP是无连接的,从一个端系统向另一个端系统发送独立的数据分组,不对交付提供任何保证。当客户或服务器程序实现了一个由RFC定义的协议时,它应当使用与该协议关联的周知端口号;与之相反,当研发一个专用应用程序时,研发者必须注意避免使用这些周知端口号。

与UDP不同,TCP是一个面向连接的协议。这意味着在客户和服务器能够开始互相发送数据之前,它们先要握手和创建一个TCP连接。TCP连接的一端与客户套接字相联系,另一端与服务器套接字相联系。当创建该TCP连接时,我们将其与客户套接字地址(IP地址和端口号)和服务器套接字地址(IP地址和端口号)关联起来。使用创建的TCP连接,当一侧要向另一侧发送数据时,它只需经过其套接字将数据丢进TCP连接。这与UDP不同,UDP服务器在将分组丢进套接字之前必须为其附上一个目的地地址。

TCP中客户程序和服务器程序的交互:客户具有向服务器发起接触的任务。服务器为了能够对客户的初始接触做出反应,服务器必须已经准备好。这意味着两件事:第一,TCP服务器在客户试图发起接触前必须作为进程运行起来。第二,服务器程序必须具有一扇特殊的门,更精确地说是一个特殊的套接字,该门欢迎来自运行在任意主机上的客户进程的某种初始接触。有时我们将客户的初始接触称为”敲欢迎之门”。随着服务器进程的运行,客户进程能够向服务器发起一个TCP连接。这是由客户程序通过创建一个TCP套接字完成的。当该客户生成其TCP套接字时,它指定了服务器中的欢迎套接字的地址,即服务器主机的IP地址及其套接字的端口号。生成其套接字后,该客户发起了一个三次握手并创建与服务器的一个TCP连接。发生在运输层的三次握手,对于客户和服务器程序是完全透明的。

在三次握手期间,客户进程敲服务器进程的欢迎之门。当该服务器”听”到敲门声时,它将生成一扇新门(更精确地讲是一个新套接字),它专门用于特定的客户。它是专门对客户进行连接的新生成的套接字,称为连接套接字。从应用程序的观点来看,客户套接字和服务器连接套接字直接通过一根管道连接,如下图所示:客户进程可以向它的套接字发送任意字节,并且TCP保证服务器进程能够按发送的顺序接收(通过连接套接字)每个字节。TCP因此在客户和服务器进程之间提供了可靠服务。此外,客户进程不仅能向它的套接字发送字节,也能从中接收字节;类似地,服务器进程不仅从它的连接套接字接收字节,也能向其发送字节。

创建客户套接字时未指定其端口号;相反,我们让操作系统为我们做此事。connect方法执行完后,执行三次握手,并在客户和服务器之间创建起一条TCP连接。在服务器调用accept方法后,客户和服务器则完成了握手。客户和服务器之间创建了一个TCP连接。借助于创建的TCP连接,客户与服务器现在能够通过该连接相互发送字节。使用TCP,从一侧发送的所有字节不仅能确保到达另一侧,而且确保按序到达。

TCP和UDP协议是以IP协议为基础的传输,为了方便多种应用程序,区分不同应用程序的数据和状态,引入了端口的概念。端口号是一个16比特的数,其大小在0~65535之间,通常称这个值为端口号。0~1023范围的端口号称为周知端口号(well-known port number),是受限制的,这是指它们保留给诸如HTTP(它使用端口号80)和FTP(它使用端口号21)之类的周知应用层协议来使用。当我们开发一个新的应用程序时,必须为其分配一个端口号。如果应用程序开发者所编写的代码实现的是一个”周知协议”的服务器端,那么开发者就必须为其分配一个相应的周知端口号。通常,应用程序的客户端让运输层自动地(并且是透明地)分配端口号,而服务器端则分配一个特定的端口号。如果是服务程序,则需要对某个端口进行绑定,这样某个客户端可以访问本主机上的此端口来与应用程序进行通信。由于IP地址只能对主机进行区分,而加上端口号就可以区分此主机上的应用程序。实际上,IP地址和端口号的组合,可以确定在网络上的一个程序通路,端口号实际上是操作系统标识应用程序的一种方法

端口号的值可由用户自定义或者由系统分配,采用动态系统分配和静态用户自定义相结合的办法。一些常用的服务程序使用固定的静态端口号,例如,Web服务器的端口号为80,电子邮件SMTP的端口号为25,文件传输FTP的端口号为20和21等。对于其他的应用程序,特别是用户自行开发的客户端应用程序,端口号采用动态分配方法,其端口号由操作系统自动分配。通常情况下,对端口的使用有如下约定,小于1024的端口未保留端口,由系统的标准服务程序使用;1024以上的端口号,用户应用程序可以使用。

套接字有三种类型:流式套接字(SOCK_STREAM)、数据报套接字(SOCK_DGRAM)及原始套接字。

(1).流式套接字:可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:”1”、”2”,那么数据到达远程时候的顺序也是”1”、”2”。Telnet是流式连接。还有WWW浏览器,它使用的HTTP协议也是通过流式套接字来获取网页的。流式套接字使用了TCP(The Transmission Control Protocol)协议。TCP保证了你的数据传输是正确的,并且是顺序的。

(2).数据报套接字:定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。它使用数据报协议UDP(User Datagram Protocol)。UDP不像流式套接字那样维护一个打开的连接,你只需要把数据打成一个包,把远程的IP贴上去,然后把这个包发送出去。这个过程是不需要建立连接的。UDP的应用例子有:tfpt, bootp等。

(3).原始套接字:主要用于一些协议的开发,可以进行比较底层的操作。

流式套接字工作过程:服务器首先启动,通过调用socket建立一个套接字,然后调用bind将该套接字和本地网络地址联系在一起,再调用listen使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用accept来接收连接。客户在建立套接字后就可调用connect和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用recv和send来接收和发送数据。最后,待数据传送结束后,双方调用close关闭套接字。

网络字节序是指多字节变量在网络传输时的表示方法,网络字节序采用大端字节序的表示方法。这样小端字节序的系统通过网络传输变量的时候需要进行字节序的转换,大端字节序的变量则不需要进行转换。字节序是由于不同的主处理器和操作系统,对大于一个字节的变量在内存中的存放顺序不同而产生的。

小端字节序(Little Endian, LE):在表示变量的内存地址的起始地址存放低字节,高字节顺序存放。LE主要用于我们现在的PC的CPU中,即Intel的x86系列兼容机。

大端字节序(Bit Endian, BE):在表示变量的内存地址的起始地址存放高字节,低字节顺序存放。

注:以上内容主要摘自于:《计算机网络自顶向下方法(原书第7版)》、《Linux网络编程(第2版)》

以下是测试代码段,可同时在Windows和Linux下执行,并对代码中用到的函数进行了说明:

const char* server_ip_ = "10.4.96.33"; // 服务器ip
const int server_port_ = 8888; // 服务器端口号,需确保此端口未被占用
			       // linux: $ netstat -nap | grep 6666; kill -9 PID
			       // windows: tasklist | findstr OpenSSL_Test.exe; taskkill /T /F /PID PID
const int server_listen_queue_length_ = 100; // 服务器listen队列支持的最大长度

以上代码段是设置的三个全局常量,server_ip_是指定测试的服务器端ip,server_port为指定的服务器端端口号,server_listen_queue_length_为指定服务器端listen队列支持的最大长度。

#ifdef _MSC_VER
// 每一个WinSock应用程序必须在开始操作前初始化WinSock的动态链接库(DLL),并在操作完成后通知DLL进行清除操作
class WinSockInit {
public:
	WinSockInit()
	{
		WSADATA wsaData;
		// WinSock应用程序在开始时必须要调用WSAStartup函数,结束时调用WSACleanup函数
		// WSAStartup函数必须是WinSock应用程序调用的第一个WinSock函数,否则,其它的WinSock API函数都将会失败并返回错误值
		int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
		if (ret != NO_ERROR)
			fprintf(stderr, "fail to init winsock: %d\n", ret);
	}

	~WinSockInit()
	{
		WSACleanup();
	}
};

static WinSockInit win_sock_init_;

#define close(fd) closesocket(fd)
#define socklen_t int
#else
#define SOCKET int
#endif

以上代码段主要是实现了WinSockInit类,此类仅在Windows下使用,用于WinSock的初始化。

int get_error_code()
{
#ifdef _MSC_VER
	auto err_code = WSAGetLastError();
#else
	auto err_code = errno;
#endif
	return err_code;
}

get_error_code函数是为了获取调用相关函数时返回的错误码,在linux使用errno,windows上不支持errno,需要使用WSAGetLastError函数。

// 服务器端处理来自客户端的数据
void calc_string_length(SOCKET fd)
{
	// 从客户端接收数据
	const int length_recv_buf = 2048;
	char buf_recv[length_recv_buf];
	std::vector<char> recved_data;

	//std::this_thread::sleep_for(std::chrono::seconds(10)); // 为了验证客户端write或send会超时

	while (1) {
		auto num = recv(fd, buf_recv, length_recv_buf, 0);
		if (num <= 0) {
			auto err_code = get_error_code();
			if (num < 0 && err_code == EINTR) {
				continue;
			}

			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to recv: %d, error code: %d, message: %s\n", num, err_code, ec.message().c_str());
			close(fd);
			return;
		}

		bool flag = false;
		std::for_each(buf_recv, buf_recv + num, [&flag, &recved_data](const char& c) {
			if (c == '\0') flag = true; // 以空字符作为接收结束的标志
			else recved_data.emplace_back(c);
		});

		if (flag == true) break;
	}

	fprintf(stdout, "recved data: ", recved_data.data());
	std::for_each(recved_data.data(), recved_data.data() + recved_data.size(), [](const char& c){
		fprintf(stdout, "%c", c);
	});
	fprintf(stdout, "\n");

	// 向客户端发送数据
	auto str = std::to_string(recved_data.size());
	std::vector<char> vec(str.size() + 1);
	memcpy(vec.data(), str.data(), str.size());
	vec[str.size()] = '\0';
	const char* ptr = vec.data();
	auto left_send = str.size() + 1; // 以空字符作为发送结束的标志

	//std::this_thread::sleep_for(std::chrono::seconds(10)); // 为了验证客户端read或recv会超时

	while (left_send > 0) {
		auto sended_length = send(fd, ptr, left_send, 0); // write
		if (sended_length <= 0) {
			int err_code = get_error_code();
			if (sended_length < 0 && err_code == EINTR) {
				continue;
			}

			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to send: %d, error code: %d, message: %s\n", sended_length, err_code, ec.message().c_str());
			close(fd);
			return;
		}

		left_send -= sended_length;
		ptr += sended_length;
	}

	close(fd);
}

以上代码段是服务器端调用的函数,服务器端程序会为每个连接上的客户程序创建一个线程,调用此函数来进行服务器端和客户端的数据的接收和发送处理。服务器端接收来自客户端的数据,并计算其长度,然后将其长度发送给客户端。

// 设置套接字为非阻塞的
int set_client_socket_nonblock(SOCKET fd)
{
#ifdef _MSC_VER
	u_long n = 1;
	// ioctlsocket: 通过将第2个参数设置为FIONBIO变更套接字fd的操作模式
	// 当此函数的第3个参数为true时,变更为非阻塞模式;为false时,变更为阻塞模式
	auto ret = ioctlsocket(fd, FIONBIO, &n);
	if (ret != 0) {
		fprintf(stderr, "fail to ioctlsocket: %d\n", ret);
		return -1;
	}
#else
	// fcntl: 向打开的套接字fd发送命令,更改其属性; F_GETFL/F_SETFL: 获得/设置套接字fd状态值; O_NONBLOCK: 设置套接字为非阻塞模式
	auto ret = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
	if (ret < 0) {
		fprintf(stderr, "fail to fcntl: %d\n", ret);
	}
#endif

	return 0;
}

// 设置套接字为阻塞的
int set_client_socket_block(SOCKET fd)
{
#ifdef _MSC_VER
	u_long n = 0;
	auto ret = ioctlsocket(fd, FIONBIO, &n);
	if (ret != 0) {
		fprintf(stderr, "fail to ioctlsocket: %d\n", ret);
		return -1;
	}
#else
	auto ret = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
	if (ret < 0) {
		fprintf(stderr, "fail to fcntl: %d\n", ret);
	}
#endif

	return 0;
}

// 设置连接超时
int set_client_connect_time_out(SOCKET fd, const sockaddr* server_addr, socklen_t length, int seconds)
{
#ifdef _MSC_VER
	if (seconds <= 0) {
#else
	if (fd >= FD_SETSIZE || seconds <= 0) {
#endif
		return connect(fd, server_addr, length);
	}

	set_client_socket_nonblock(fd);

	auto ret = connect(fd, server_addr, length);
	if (ret == 0) {
		set_client_socket_block(fd);
		fprintf(stdout, "non block connect return 0\n");
		return 0;
	}
#ifdef _MSC_VER
	else if (ret == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) {
#else
	else if (ret < 0 && errno != EINPROGRESS) {
#endif
		fprintf(stderr, "non block connect fail return: %d\n", ret);
		return -1;
	}

	// 设置超时
	fd_set fdset;
	FD_ZERO(&fdset);
	FD_SET(fd, &fdset);

	struct timeval tv;
	tv.tv_sec = seconds;
	tv.tv_usec = 0;

	// select: 非阻塞方式,返回值:0:表示超时; 1:表示连接成功; -1:表示有错误发生
	// 注:在windows下select函数不作为计时器,在windows下,select的第一个参数可以忽略,可以是任意值
	ret = select(fd + 1, nullptr, &fdset, nullptr, &tv);
	if (ret < 0) {
		fprintf(stderr, "fail to select: %d\n", ret);
		return -1;
	} else if (ret == 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "connect time out: error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
		return -1;
	} else {
		int optval;
		socklen_t optlen = sizeof(optval);
#ifdef _MSC_VER
		ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, (char*)&optval, &optlen);
#else
		// getsockopt: 获得套接字选项设置情况,此函数的第3个参数SO_ERROR表示获取错误
		ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
#endif
		if (ret == -1 || optval != 0) {
			fprintf(stderr, "fail to getsockopt\n");
			return -1;
		}

		if (optval == 0) {
			set_client_socket_block(fd);
			fprintf(stdout, "connect did not time out\n");
			return 0;
		}
	}

	return 0;
}

以上代码段是用来设置客户端连接超时,通过调用select函数作为计时器,默认的connect、accept、recv、send函数都是属于阻塞方式,而select是非阻塞方式。select一般作为计时器仅用于非Windows平台,因为select的第一个参数套接字在windows上不起作用。

int set_client_send_time_out(SOCKET fd, int seconds)
{
	if (seconds <= 0) {
		fprintf(stderr, "seconds should be greater than 0: %d\n", seconds);
		return -1;
	}

#ifdef _MSC_VER
	DWORD timeout = seconds * 1000; // milliseconds
	auto ret = setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
#else
	struct timeval timeout;
	timeout.tv_sec = seconds;
	timeout.tv_usec = 0;

	// setsockopt: 设置套接字选项,为了操作套接字层的选项,此函数的第2个参数的值需指定为SOL_SOCKET,第3个参数SO_SNDTIMEO表示发送超时,第4个参数指定超时时间
	// 默认情况下send函数在发送数据的时候是不会超时的,当没有数据的时候会永远阻塞
	auto ret = setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
#endif
	if (ret < 0) {
		fprintf(stderr, "fail to setsockopt: send\n");
		return -1;
	}

	return 0;
}

// 设置接收数据recv超时
int set_client_recv_time_out(SOCKET fd, int seconds)
{
	if (seconds <= 0) {
		fprintf(stderr, "seconds should be greater than 0: %d\n", seconds);
		return -1;
	}

#ifdef _MSC_VER
	DWORD timeout = seconds * 1000; // milliseconds
	auto ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
#else
	struct timeval timeout;
	timeout.tv_sec = seconds;
	timeout.tv_usec = 0;

	// setsockopt: 此函数的第3个参数SO_RCVTIMEO表示接收超时,第4个参数指定超时时间
	// 默认情况下recv函数在接收数据的时候是不会超时的,当没有数据的时候会永远阻塞
	auto ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
#endif
	if (ret < 0) {
		fprintf(stderr, "fail to setsockopt: recv\n");
		return -1;
	}

	return 0;
}

以上代码段是用来设置客户端接收和发送数据超时,主要通过setsockopt函数实现。

int test_socket_tcp_client()
{
	// 1.创建流式套接字
	auto fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
		return -1;
	}

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(server_port_);
	auto ret = inet_pton(AF_INET, server_ip_, &server_addr.sin_addr);
	if (ret != 1) {
		fprintf(stderr, "fail to inet_pton: %d\n", ret);
		return -1;
	}

	set_client_send_time_out(fd, 2); // 设置write或send超时时间
	set_client_recv_time_out(fd, 2); // 设置read或recv超时时间

	// 2.连接
	// connect函数的第二参数是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型
	ret = set_client_connect_time_out(fd, (struct sockaddr*)&server_addr, sizeof(server_addr), 2); // 设置连接超时时间
	//ret = connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret != 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to connect: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
		return -1;
	}

	// 3.接收和发送数据
	// 向服务器端发送数据
	const char* buf_send = "https://blog.csdn.net/fengbingchun";
	const char* ptr = buf_send;
	auto length = strlen(buf_send);
	auto left_send = length + 1; // 以空字符作为发送结束的标志

	// 以下注释掉的code仅用于测试write或send超时时间
	//std::unique_ptr<char> buf_send(new char[1024 * 1024]);
	//int length = 1024 * 1024;
	//long long count = 0;
	//for (;;) {
		//int left_send = length + 1;
		//const char* ptr = buf_send.get();
		//fprintf(stdout, "count: %lld\n", ++count);
		while (left_send > 0) {
			// send: 将缓冲区ptr中大小为left_send的数据,通过套接字文件描述符fd按照第4个参数flags指定的方式发送出去
			// send的返回值是成功发送的字节数.由于用户缓冲区ptr中的数据在通过send函数进行发送的时候,并不一定能够
			// 全部发送出去,所以要检查send函数的返回值,按照与计划发送的字节长度left_send是否相等来判断如何进行下一步操作
			// 当send的返回值小于left_send的时候,表明缓冲区中仍然有部分数据没有成功发送,这是需要重新发送剩余部分的数据
			// send发生错误的时候返回值为-1
			// 注意:send的成功返回并不一定意味着数据已经送到了网络中,只说明协议栈有足够的空间缓存数据,协议栈可能会为了遵循协议的约定推迟传输
			auto sended_length = send(fd, ptr, left_send, 0); // write
			if (sended_length <= 0) {
				auto err_code = get_error_code();
				if (sended_length < 0 && err_code == EINTR) {
					continue;
				}

				std::error_code ec(err_code, std::system_category());
				fprintf(stderr, "fail to send: %d, err code: %d, message: %s\n", sended_length, err_code, ec.message().c_str());
				return -1;
			}
			left_send -= sended_length;
			ptr += sended_length;
		}
	//}

	// 从服务器端接收数据
	const int length_recv_buf = 2048;
	char buf_recv[length_recv_buf];
	std::vector<char> recved_data;
	while (1) {
		// recv: 用于接收数据,从套接字fd中接收数据放到缓冲区buf_recv中,第4个参数用于设置接收数据的方式
		// recv的返回值是成功接收到的字节数,当返回值为-1时错误发生
		auto num = recv(fd, buf_recv, length_recv_buf, 0); // read
		if (num <= 0) {
			auto err_code = get_error_code();
			if (num < 0 && err_code == EINTR) {
				continue;
			}

			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to recv: %d, err code: %d, message: %s\n", num, err_code, ec.message().c_str());
			return -1;
		}

		bool flag = false;
		std::for_each(buf_recv, buf_recv + num, [&flag, &recved_data](const char& c) {
			if (c == '\0') flag = true; // 以空字符作为接收结束的标志
			else recved_data.emplace_back(c);
		});

		if (flag == true) break;
	}

	// 4.关闭套接字
	close(fd);

	// 验证接收的数据是否是预期的
	fprintf(stdout, "send data: %s\n", buf_send, recved_data.data());
	fprintf(stdout, "recved data: ");
	std::for_each(recved_data.data(), recved_data.data() + recved_data.size(), [](const char& c){
		fprintf(stdout, "%c", c);
	});
	fprintf(stdout, "\n");

	std::string str(recved_data.data());
	auto length2 = std::stoi(str);
	if (length != length2) {
		fprintf(stderr, "received data is wrong: %d, %d\n", length, length2);
		return -1;
	}

	return 0;
}

以上代码段是客户端程序实现,同时支持在Windows和Linux上运行。

int test_socket_tcp_server()
{
	// 1.创建流式套接字
	// socket:参数依次为协议族、协议类型、协议编号. AF_INET: 以太网;
	// SOCK_STREAM:流式套接字,TCP连接,提供序列化的、可靠的、双向连接的字节流
	auto fd = socket(AF_INET, SOCK_STREAM, 0);
	if (fd < 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());
		return -1;
	}

	// 2.绑定地址端口
	// sockaddr_in: 以太网套接字地址数据结构,与结构sockaddr大小完全一致
	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	// htons: 网络字节序转换函数,还包括htonl, ntohs, ntohl等,
	// 其中s是short数据类型的意思,l是long数据类型的意思,而h是host,即主机的意思,n是network,即网络的意思,
	// htons: 表示对于short类型的变量,从主机字节序转换为网络字节序
	server_addr.sin_port = htons(server_port_);
	// inet_xxx: 字符串IP地址和二进制IP地址转换函数
	// inet_pton: 将字符串类型的IP地址转换为二进制类型,第1个参数表示网络类型的协议族
	auto ret = inet_pton(AF_INET, server_ip_, &server_addr.sin_addr);
	if (ret != 1) {
		fprintf(stderr, "fail to inet_pton: %d\n", ret);
		return -1;
	}

	// sockaddr: 通用的套接字地址数据结构,它可以在不同协议族之间进行转换,包含了地址、端口和IP地址的信息
	ret = bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret != 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to bind: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
		return -1;
	}

	//std::this_thread::sleep_for(std::chrono::seconds(30)); // 为了验证客户端连接会超时

	// 3.监听端口
	// listen: 用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接.
	// 当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度有listen函数来定义
	// listen的第二个参数表示在accept函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一个错误
	ret = listen(fd, server_listen_queue_length_);
	if (ret != 0) {
		auto err_code = get_error_code();
		std::error_code ec(err_code, std::system_category());
		fprintf(stderr, "fail to listen: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());
		return -1;
	}

	while (1) {
		struct sockaddr_in client_addr;
		socklen_t length = sizeof(client_addr);
		// 4.接收客户端的连接,在这个过程中客户端与服务器进行三次握手,建立TCP连接
		// accept成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新描述符来获得
		// 当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接
		auto fd2 = accept(fd, (struct sockaddr*)&client_addr, &length);
		if (fd2 < 0) {
			auto err_code = get_error_code();
			std::error_code ec(err_code, std::system_category());
			fprintf(stderr, "fail to accept: %d, error code: %d, message: %s\n", fd2, err_code, ec.message().c_str());
			continue;
		}
		struct in_addr addr;
		addr.s_addr = client_addr.sin_addr.s_addr;
		// inet_ntoa: 将二进制类型的IP地址转换为字符串类型
		fprintf(stdout, "client ip: %s\n", inet_ntoa(addr));

		// 5.接收和发送数据
		// 连接上的每一个客户都有单独的线程来处理
		std::thread(calc_string_length, fd2).detach();
	}

	// 关闭套接字
	close(fd);
	return 0;
}

以上代码段是服务器端程序实现,同时支持Windows和Linux上运行。

以下是Linux作为服务器端,Windows作为客户端时的执行结果:服务器端程序先启动

以上测试的完整代码见:GitHub OpenSSL_Test/funset_socket.cpp

GitHub:https://github.com/fengbingchun/OpenSSL_Test

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Windows/Linux TCP Socket网络编程简介及测试代码 的相关文章

  • 关于pyqt的一些用法

    QT原生是C 43 43 xff0c pyqt基于python语言 关于插件 xff1a 安装一个PyUIC xff0c 一个Qt Designer 点击Qt Designer可以出来ui配置页面 xff0c 和qt原生IDE基本一样 上面
  • python从单独的文件中获取配置信息

    可以新建一个configure json文件 xff0c 里面放置json格式的字符串 比如 xff1a deviceMAC1 12412421413 deviceMAC 12413232 name 123123 使用下述程序即可获取jso
  • 解决串口工具串口号改变的问题(借助python和ch344/8)

    串口工具一般在插不同usb口的时候 xff0c 识别出来的串口号不一致 xff0c pl2303可以固定 xff0c 但是更换电脑后也会变化 xff0c 可以借助沁恒家的一款串口一转多的工具 xff0c ch344或ch348 xff0c
  • python将监听到的串口信息实时保存到日志

    尽量在一个单独的线程中运行 stim thread span class token operator 61 span threading span class token punctuation span Thread span clas
  • 技能梳理37@stm32+按键+光耦+锁存+串行转并行+继电器

    技能梳理37 64 stm32 43 按键 43 光耦 43 锁存 43 串行转并行 43 继电器 1 项目简介 2 实现逻辑 3 应用场景 主从机有线控制 开关操作 4 核心代码梳理 span class token comment 根据
  • 技能梳理38@stm32+CC2530+超声波+光敏+oled

    技能梳理38 64 stm32 43 CC2530 43 超声波 43 光敏 43 oled 1 项目简介 2 实现逻辑 主从机使用zigbee通信 主机oled显示当前停车场位置图 从机通过超声波和光敏一起检测是否有车 当某位置车辆进出
  • 图文手把手教程--ESP32 OTA空中升级(VSCODE+IDF)

    本文内容 1 xff09 使用hello world例程 xff0c 编译生成hello world bin文件 xff0c 并且开启HTTP本地服务器 2 xff09 使用simple ota example例程 xff0c 通过HTTP
  • HTML URL 编码参考手册-------转载

    一 基础知识点 xff1a web浏览器通过URL从WEB服务器请求页面 在线URL编码解码工具 UrlEncode编码 UrlDecode解码在线工具 二 URL编码作用 xff1a 1 URL编码将字符转换为可通过因特网传输的格式 xf
  • 第28章 LTDC—液晶显示中英文

    本章参考资料 xff1a STM32F76xxx参考手册 STM32F7xx 规格书 库帮助文档 STM32F779xx User Manual chm 关于开发板配套的液晶屏参数可查阅 5 0寸液晶屏数据手册 xff08 或 7 0 寸数
  • SFTP get数据提示错误:cannot download non-regular file

    原因 xff1a 这是由于sftp基本客户端不允许进行递归下载 xff0c 或者是对一个文件夹下载 解决方案 xff1a 需要进去目录下载一个具体的文件 可以看看这个报错是否由于自己讲一个文件夹当成了自己需要下载的文件进行了下载
  • 使用wireshark过滤/查找协议里面的特定内容

    一 使用wireshark命令过滤 xff1a 1 tcp的载荷 xff1a tcp contains 34 api 34 说明 xff1a 在tcp报文中过滤出含有 api 内容的报文 xff1b 如下图 xff1a 2 其它类型协议同理
  • 两步解决科来数据包生成器找不到网卡的问题

    1 下载科来数据包生成器 xff0c 按照默认配置安装即可 xff1a http www colasoft com cn download packet builder php 2 部分同学安装完成后 xff0c 在准备发包时会发现 xff
  • 离线安装selenium

    1 在线情况下最简单直接cmd xff1a cmd环境下 xff0c 用pip install selenium命令安装即可 2 离线安装 xff1a xff08 1 xff09 https pypi org project seleniu
  • vim命令进阶

    显示当前位置行数 nu 显示所有行数 set nu 删除n m行 n m d 删除n 结尾行 n d
  • Centos2步配置本地yum源

    1 进入目录 xff0c 将其中的原有yum源配置转到备份目录当中 cd etc yum repos d mkdir backup mv repo backup 2 新建本地的yum源配置文件 vim local repo 输入一下内容 j
  • Linux4步快速搭建DNS服务器

    1 下载DNS服务器软件 xff1a yum install y bind 2 配置基本配置文件 xff1a vim etc named conf 3 配置域名文件 xff1a vim var named db cn zone 4 重启服务
  • 海康OSD水印

    设置 OSD 字符叠加参数 64 param iUserID loginId 64 param iChan 通道号 64 param contents 内容 public static void SetOSDString int iUser
  • STM32芯片配置错误SWD功能被占用时的下载方法.

    最近在用STM32CubeMX配置STM32芯片 可以配置时钟 管脚 SPI串口等 免去了复杂的计算过程 不用记各种功能寄存器 相当的方便 用过的都说好 强烈推荐大家一试 但刀太锋利了用不熟练也容易误伤 在分配IO口功能的过程中 忘记了SW
  • STM32核心的电源管理器程序卡死现象的分析与解决

    某个项目中的电源管理器在外场频繁出现通讯中卡死现象 该电源管理器内部有多路DC DC主要起电源适配 电源管理及监控作用 控制核心为STM32F103RET7 主要任务为AD采集16路模拟电压 xff0c 控制4路电源通断 xff0c 2路串
  • 第29章 电容触摸屏—触摸画板

    本章参考资料 xff1a STM32F76xxx参考手册 STM32F7xx 规格书 库帮助文档 STM32F779xx User Manual chm 关于开发板配套的触摸屏参数可查阅 5 0寸触摸屏面板说明 xff0c 触摸面板配套的触

随机推荐

  • SBUS2 协议初探

    SBUS2和SBUS的区别 1 xff1a 总线类型不一样 sbus是单向传输 xff0c 只做控制使用 xff0c 不做输入 SBUS2是非标准的双向总线 xff0c 可以做控制输出 xff0c 也可以做信息输入 2 xff1a 结束符不
  • IAR 中如何调用EmEditor

    IAR 中如何调用EmEditor 文本编辑器我一直都用EmEditor Emeditor是一款很优秀的纯文本编辑器 xff0c 它对中日韩等亚洲字符集支持的很好 xff0c 不仅有语法高亮显示功能 xff0c 搜索替换功能也是近乎可爱的强
  • 花了一下午,弄明白四个名词 USCI,USI,LIN,IrDa

    最近在看MSP的程序 xff0c TI的官方例子程序 xff0c 有些名词看了却不知道什么意思 xff0c 本着穷根问底的态度 xff0c 上网搜索 xff0c 花了一下午 xff0c 弄明白四个名词 USCI xff0c USI xff0
  • Cadence 17.2学习笔记--显示元件丝印及元件标号的设置

    快捷键Ctrl 43 F5 调出Color Dialog 左侧菜单第三项Geometry选项 钩选图中Silkscreen Top或Silkscreen Bottom可打开或关闭元件外形丝印的显示 同样Color Dialog对话框 左侧菜
  • 【Android UI】色板

    Hex CodeColor FFFFFF FFFFCC FFFF99 FFFF66 FFFF33 FFFF00 FFCCFF FFCCCC FFCC99 FFCC66 FFCC33 FFCC00 FF99FF FF99CC FF9999 F
  • 对称加密算法AES简介及在OpenSSL中使用举例

    高级加密标准 AES Advanced Encryption Standard 由美国国家标准和技术协会 NIST 于2000年公布 xff0c 它是一种对称加密算法 xff0c 用来替代DES AES也称为Rijndael算法 xff0c
  • 基于Hash的消息认证码HMAC简介及在OpenSSL中使用举例

    HMAC Hash based Message Authentication Code xff1a 基于Hash的消息认证码 xff0c 是一种通过特别计算方式之后产生的消息认证码 MAC xff0c 使用密码散列函数 xff0c 同时结合
  • 在Windows和Linux上编译gRPC源码操作步骤(C++)

    gRPC最新发布版本为v1 23 0 xff0c 下面以此版本为例说明在Windows和Linux下编译过程 Windows7 10 vs2103编译gRPC源码操作步骤 xff1a 1 需要本机已安装Git CMake Perl Go y
  • gRPC简介及简单使用(C++)

    gRPC是一个现代的 开源的 高性能远程过程调用 RPC 框架 xff0c 可以在任何平台运行 gRPC使客户端和服务器端应用程序能够透明地进行通信 xff0c 并简化了连接系统的构建 gRPC支持的语言包括C 43 43 Ruby Pyt
  • C语言中select函数简介及使用

    select函数用来检查套接字描述符 sockets descriptors 是否已准备好读 写 xff0c 提供了一种同时检查多个套接字的方法 Linux中select函数的声明在 usr include x86 64 linux gnu
  • VS Code离线安装C/C++插件cpptools-linux-aarch64.vsix

    一 问题 最近VS Code连接jetson nano xff0c 经常提示加载C C 43 43 插件失败 二 解决方法 根据提示 xff0c 在githup上 https github com microsoft vscode cppt
  • 开源库BearSSL介绍及使用

    BearSSL是用C语言实现的SSL TLS协议 xff0c 它的源码可直接通过git clone https www bearssl org git BearSSL 下载 xff0c 它的license是MIT xff0c 最新版本为0
  • 对称加密算法AES之GCM模式简介及在OpenSSL中使用举例

    AES Advanced Encryption Standard 即高级加密标准 xff0c 由美国国家标准和技术协会 NIST 于2000年公布 xff0c 它是一种对称加密算法 关于AES的更多介绍可以参考 xff1a https bl
  • ASN.1简介及OpenSSL中ASN.1接口使用举例

    ASN 1 Abstract Syntax Notation One 是一套标准 xff0c 是描述数据的表示 编码传输 解码的灵活的记法 它提供了一套正式 无歧义和精确的规则以描述独立于特定计算机硬件的对象结构 OpenSSL的编码方法就
  • 从openssl rsa pem文件中提取公私钥数据实现

    RSA为非对称加密算法 xff0c 关于其介绍可以参考 xff1a https blog csdn net fengbingchun article details 43638013 OpenSSL最新版为 1 1 1g xff0c 在Wi
  • 通过OpenSSL的接口实现Base64编解码

    对openssl genrsa产生的rsa私钥pem文件 xff0c 使用普通的base64解码会有问题 xff0c 如使用https blog csdn net fengbingchun article details 85218653
  • JWT(JSON Web Token)简介及实现

    JWT JSON Web Token xff1a 是一个开放标准 RFC 7519 xff0c 它定义了一种紧凑且自包含的方式 xff0c 用于在各方之间作为Json对象安全地传输信息 由于此信息是经过数字签名的 xff0c 因此可以被验证
  • SSL/TLS单向认证和双向认证介绍

    为了便于理解SSL TLS的单向认证和双向认证执行流程 xff0c 这里先介绍一些术语 1 散列函数 Hash function xff1a 又称散列算法 哈希函数 xff0c 是一种从任何一种数据中创建小的数字 指纹 的方法 散列函数把消
  • 使用OpenSSL生成自签名证书相关命令

    在用于小范围测试等目的的时候 xff0c 用户也可以自己生成数字证书 xff0c 但没有任何可信赖的机构签名 xff0c 此类数字证书称为自签名证书 证书一般采用X 509标准格式 下面通过OpenSSL相关命令介绍如何生成自签证书 1 生
  • Windows/Linux TCP Socket网络编程简介及测试代码

    典型的网络应用是由一对程序 即客户程序和服务器程序 组成的 xff0c 它们位于两个不同的端系统中 当运行这两个程序时 xff0c 创建了一个客户进程和一个服务器进程 xff0c 同时它们通过从套接字 socket 读出和写入数据在彼此之间