在类Unix平台实现TCP服务端

在类Unix平台实现TCP客户端

创建服务器socket

在TCP服务器代码中,我们创建一个socket,然后调用bind函数,绑定到这个socket:

	// 创建本地地址配置信息
	struct addrinfo hints;
	// 清空hints的东西,为设置新的信息做准备
	memset(&hints,0,sizeof(hints));
	// TCP,选用SOCK_STREAM
	hints.ai_socktype = SOCK_STREAM;
	// 因为此地址将用作监听socket的地址,因此设置被动状态
	hints.ai_flags = AI_PASSIVE;
	// 返回的本地地址信息
	struct addrinfo *local_address;
	// getaddrinfo函数初始化本地地址信息,端口设置为8899
	if(getaddrinfo(NULL,"8899",&hints,&local_address)){
		fprintf(stderr,"getaddrinfo() failed. (%d)\n",errno);
		return 1;
	}

	printf("Creating a socket...\n");
	// 使用配置好的本地地址信息创建socket
	int server_socket = socket(local_address->ai_family,local_address->ai_socktype,local_address->ai_protocol);
	if(server_socket == -1){
		fprintf(stderr,"socket() failed. (%d)\n",errno);
		return 1;
	}

我们没有指定hints.ai_family = AF_INET或 AF_INET6,因为getaddrinfo可以动态决定它的具体类型,也就是我们可以同时兼容IPv4 和IPv6.

绑定socket到一个本地地址

上一步只是用本地地址信息创建了socket,还要将这个socket与本地地址绑定起来,才能真正关联起来。由bind函数来完成

printf("Binding socket to local address...\n");
	if(bind(server_socket,local_address->ai_addr,local_address->ai_addrlen)){
		fprintf(stderr,"bind() failed. (%d)\n",errno);
		return 1;
	}
	// 绑定完成后,本地地址信息后面就不会再使用了,于是我们释放掉它,节省内存
	freeaddrinfo(local_address);

让socket进入监听状态

调用listen函数让socket可以监听外界对它的访问。我们这里设置了最多有10个等待处理的进来的访问,换句话说,

	printf("Listening...\n");
	if(listen(server_socket,10) == -1){
		fprintf(stderr,"listen() failed. (%d)\n",errno);
		return 1;
	}

使用select函数处理socket的同步

这一段是最精彩的。

	// 初始化fd_set文件描述符集合
	fd_set master_set;
	// 清空文件描述符集合里的东西
	FD_ZERO(&master_set);
	// 将刚刚创建的用于监听的socket加入到集合中,当这个socket需要处理进来的请求时,就可以select函数中返回
	FD_SET(server_socket,&master_set);
	int fdmax;
	fdmax = server_socket;
	// 无限循环,这是正常的,因它是主程序,不能够退出。
	while(1){
		// 定义一个读文件描述符集合
	    fd_set read_fds;
	    // 清0集合
		FD_ZERO(&read_fds);
		// 将master_set复制一份到read_fds
		memcpy(&read_fds,&master_set,sizeof(master_set));
		// select函数将read_fds传进去,第一个参数是最大socket编号加1,read_fds包含所有需要监听读变化的文件描述符,如果有变化就会通过read_fds返回(只包含有读变化的socket,进去是全部socket,出来时只有部分),所以前面需要将master_set复制一份到read_fds。select是一个阻塞的函数,除非有变化,否则就卡在这了,这样省了很多资源的。
		if(select(fdmax+1,&read_fds,0,0,0) == -1){
			fprintf(stderr,"select() failed. (%d)\n",errno);
			return 1;
		}
		// 来到这一步,说明有变化了,遍历一遍socket
		for(int i = 0;i <= fdmax;i++){
			// 检查socket是否在返回的读文件描述符集合中
			if(FD_ISSET(i,&read_fds)){
				// 如果在读文件描述符集合中,那么看看是否是监听用的socket,即服务端socket
				if(i == server_socket){
				// 看来是有新的访问要建立连接,处理新连接
					// 记录对端(客户端)socket的信息
					struct sockaddr_storage peer_address;
					socklen_t peer_address_size = sizeof(peer_address);
					// accept函数创建对端(客户端)socket
					int peer_socket = accept(server_socket,(struct sockaddr*)&peer_address,&peer_address_size);
					if(peer_socket == -1){
						fprintf(stderr,"accept() failed. (%d)\n",errno);
						return 1;
					}
					// 将客户端socket放入文件描述符集合中,以便与其通信
					FD_SET(peer_socket,&master_set);
					if(peer_socket > fdmax){
						fdmax = peer_socket;
					}
					char address_buffer[100];
					// 打印客户端的信息
					getnameinfo((struct sockaddr*)&peer_address,peer_address_size,address_buffer,sizeof(address_buffer),0,0,NI_NUMERICHOST);
					printf("Accepted connection on descriptor %d (host=%s)\n",peer_socket,address_buffer);
				}
				else{
					//处理客户端来的信息
					char read[1024]; // 准备一个接收信息字符数组
					// 接收客户端的数据
					int bytes_read = recv(i,read,sizeof(read),0);
					if(bytes_read <= 0){
					// 如果接收到数据小于或等于0,那么意味着客户端要关闭
						//connection closed by client
						printf("Terminated connection on descriptor %d\n",i);
						// 将客户端socket从master_set中移除
						FD_CLR(i,&master_set);
						// 在服务端这边关闭客户端socket
						close(i);
						continue;
					}
					printf("Received message (%d bytes): %s\n",bytes_read,read);
					///
					//echo message back to client
					for (int j = 0; j < bytes_read; j++)
					{
						read[j] = toupper(read[j]);
					}
					printf("Sending message (%d bytes): %s\n",bytes_read,read);
					// 向客户端发送数据
					send(i,read,bytes_read,0);
					
				}
			}
		}
	}

聊天室

					///
					//echo message back to client
					for (int j = 0; j < bytes_read; j++)
					{
						read[j] = toupper(read[j]);
					}
					printf("Sending message (%d bytes): %s\n",bytes_read,read);
					// 向客户端发送数据
					send(i,read,bytes_read,0);
					

将上述代码,用下面的代码代替,就可以将程序变成聊天室。聊天室就要将信息发给每一个客户端,服务端和自己是不需要收到发的信息的。


for(int j = 0; j <= fdmax; j++){
	// 检查socket中在不在master_set
	if(FD_ISSET(j,&master_set)){
		// 如果是服务端socket则进入下一个
		if(j == server_socket){
			continue;
		}
		// 如果是自己,即同一个socket则进入下一轮
		if(j == i){
			continue;
		}
		// 给其他客户端发信息
		send(j,read,bytes_read,0);
	}
}

处理send函数阻塞的问题

当我们调用send函数向对端发送数据时,send函数会将数据拷贝到一个由操作系统提供的输出缓存中。如果调用send函数时输出缓存已满,那么send函数就会阻塞,直到输出缓存有足够空间接收数据。

当输出缓存已满,那么send函数就会阻塞在那,等待输出缓存有空的空间接收数据。
当输出缓存有部分空闲的空间,但不足以接收完所有要发送的数据,在这种情况下,send会返回一个值,这个值表示有多少比特已被拷贝进输出缓存了。比如说我们的程序现在在send函数这里阻塞了,然后收到了来自操作系统的一个信号。有些数据已被拷贝进去,还剩下部分,此时要不要再继续发送剩下的数据取决于我们了。所以在一个健壮应用中,我们需要去对比从send函数返回的值(这个值代表实际拷贝进入输出缓存的比特数)与我们要发送数据的比特数。如果相等,表示全部发送完,如果返回的值小于我们要发送的数量,就是说还剩下一部分数据没有发送,那么我们要调用select函数,看看socket是否已准备好接收新的数据,一旦从select函数获知socket已准备好接收新数据,我们调用send函数发送剩下的数据。这是一个健壮的应用应有的做法,虽然会有点复杂。

一般来说我们发的数据都不会超过操作系统提供的输出缓存大小,但是假如服务器就是要向客户端socket发送大量的数据,大到超出输出缓存大小的量,那么我们更要检查send函数返回的值,更要与要发送的量作比较,确定剩下多少数据要再发送。这些工作确实会有些复杂,尤其是在服务端,它必须跟踪所有的socket的状态,维护各自要发送的数据。我们以一个小片断来说明这个想法应该没有问题:

// 用start记录我们发送了多少数据
int start = 0;
// buffer_requested表示我们要发送的比特量
while(start < buffer_requested) {
	// sent表示已成功拷贝进输出缓存的比特量,buffer_requested - start表示要发送的量,第一次时,是全部一次发,如果第一次没有拷贝完,那么buffer_requested - start表示剩余要拷贝的量
	int sent = send(peer_socket,buffer+start,buffer_requested - start,0);
	if(sent == -1) {
		// 当send函数遇到错误时,我们就可以终止所有数据的拷贝了
		// 在这个处理send函数的错误
	}
	start += sent; // 累加已拷贝的量,记录在start变量中,表示到目前为止已拷贝进输出缓存的比特量
}

上述这段代码呢,就是一直阻塞,直到发送完所有数据。在更复杂的环境中,需要有更好的方式来完成上述工作。因为如果数据量太庞大,那么其他socket就得不到服务了。系统看起来就像卡死了一样。比方说,如果我们要管理大量的socket,又不想阻塞,其实也还是通过select函数来解决。以下是select函数的原型:

int select(int nfds, fd_set *_Nullable restrict readfds,
                  fd_set *_Nullable restrict writefds,
                  fd_set *_Nullable restrict exceptfds,
                  struct timeval *_Nullable restrict timeout);

解释一下这些参数的作用:
select函数的传入的参数和返回的参数都是同一个,所以我们看到它的参数类型都是指针类型;

  • readfds:传入时,包括所有需要等待读准备好的文件描述符(在这里就是所有的socket),返回时readfds只包含读准备好的socket
  • writefds: 传入时,包括所有需要等待写准备好的文件描述符(在这里就是所有的socket),返回时writefds只包含写准备好的socket

所以在我们处理send函数时,就是要写入输出缓存的,因为我们只需要把等写准备好的socket放到writefds中,当select返回时,查询要写的socket在不在writefds中,如果在,说明socket已为写做好准备了,那么我们就可以把剩下要发送的数据继续发送。

while(1){
        ...
	    fd_set writefds;
		FD_ZERO(&writefds);
		memcpy(&writefds,&master_set,sizeof(master_set));
		if(select(fdmax+1,&read_fds,&writefds,0,0) == -1){
			fprintf(stderr,"select() failed. (%d)\n",errno);
			return 1;
		}
		for(int i = 0;i <= fdmax;i++){
			...
			if(FD_ISSET(i,&writefds)){
				if(i == server_socket){
					......
				}
				else{
					......
					// sent表示已成功拷贝进输出缓存的比特量,buffer_requested - start表示要发送的量,第一次时,是全部一次发,如果第一次没有拷贝完,那么buffer_requested - start表示剩余要拷贝的量
					int sent = send(peer_socket,buffer+start,buffer_requested - start,0);
					if(sent == -1) {
						// 当send函数遇到错误时,我们就可以终止所有数据的拷贝了
						// 在这个处理send函数的错误
					}
					start += sent; // 累加已拷贝的量,记录在start变量中,表示到目前为止已拷贝进输出缓存的比特量
				}
			}
		}

上面的代码,大概地表达了我们的思路。每个socket剩余要发送的比特数据和量都需要有一个地方妥善地维护(这一点就不在这里讨论了。)select模型确实是一个很不错的实现。

完整代码

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>

int main(int argc,char *argv[]){
	printf("Configuring local address...\n");
	struct addrinfo hints;
	memset(&hints,0,sizeof(hints));
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE;
	
	struct addrinfo *local_address;
	if(getaddrinfo(NULL,"8080",&hints,&local_address)){
		fprintf(stderr,"getaddrinfo() failed. (%d)\n",errno);
		return 1;
	}

	printf("Creating a socket...\n");
	int server_socket = socket(local_address->ai_family,local_address->ai_socktype,local_address->ai_protocol);
	if(server_socket == -1){
		fprintf(stderr,"socket() failed. (%d)\n",errno);
		return 1;
	}

	printf("Binding socket to local address...\n");
	if(bind(server_socket,local_address->ai_addr,local_address->ai_addrlen)){
		fprintf(stderr,"bind() failed. (%d)\n",errno);
		return 1;
	}
	freeaddrinfo(local_address);

	printf("Listening...\n");
	if(listen(server_socket,10) == -1){
		fprintf(stderr,"listen() failed. (%d)\n",errno);
		return 1;
	}
	fd_set master_set;
	FD_ZERO(&master_set);
	FD_SET(server_socket,&master_set);
	int fdmax;
	fdmax = server_socket;
	
	while(1){
	    fd_set read_fds;
		FD_ZERO(&read_fds);
		memcpy(&read_fds,&master_set,sizeof(master_set));
		if(select(fdmax+1,&read_fds,0,0,0) == -1){
			fprintf(stderr,"select() failed. (%d)\n",errno);
			return 1;
		}
		for(int i = 0;i <= fdmax;i++){
			if(FD_ISSET(i,&read_fds)){
				if(i == server_socket){
					//handle new connection
					struct sockaddr_storage peer_address;
					socklen_t peer_address_size = sizeof(peer_address);
					int peer_socket = accept(server_socket,(struct sockaddr*)&peer_address,&peer_address_size);
					if(peer_socket == -1){
						fprintf(stderr,"accept() failed. (%d)\n",errno);
						return 1;
					}
					FD_SET(peer_socket,&master_set);
					if(peer_socket > fdmax){
						fdmax = peer_socket;
					}
					char address_buffer[100];
					getnameinfo((struct sockaddr*)&peer_address,peer_address_size,address_buffer,sizeof(address_buffer),0,0,NI_NUMERICHOST);
					printf("Accepted connection on descriptor %d (host=%s)\n",peer_socket,address_buffer);
				}
				else{
					//handle data from client
					char read[1024];
					int bytes_read = recv(i,read,sizeof(read),0);
					if(bytes_read <= 0){
						//connection closed by client
						printf("Terminated connection on descriptor %d\n",i);
						FD_CLR(i,&master_set);
						close(i);
						continue;
					}
					printf("Received message (%d bytes): %s\n",bytes_read,read);
					//echo message back to client
					// for (int j = 0; j < bytes_read; j++)
					// {
					// 	read[j] = toupper(read[j]);
					// }
					// printf("Sending message (%d bytes): %s\n",bytes_read,read);
					// send(i,read,bytes_read,0);
					for(int j = 0; j <= fdmax; j++){
						if(FD_ISSET(j,&master_set)){
							if(j == server_socket){
								continue;
							}
							if(j == i){
								continue;
							}
							send(j,read,bytes_read,0);
						}
					}
				}
			}
		}
	}
	printf("Closing socket\n");
	close(server_socket);
	printf("Finished.\n");
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值