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

典型的网络应用是由一对程序(即客户程序和服务器程序)组成的,它们位于两个不同的端系统中。当运行这两个程序时,创建了一个客户进程和一个服务器进程,同时它们通过从套接字(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

GitHubhttps://github.com/fengbingchun/OpenSSL_Test

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值