网络IO编程(如何实现客户端服务器之间TCP通信)

1. 单客户端单服务器TCP通信

服务端

include <sys/socket.h>
#include <error.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

#define BUFFER_LENGTH 1024

int main(){
	// 创建socket:int socket(int domain, int type, int protocol)
	// domain参数告诉系统使用哪个底层协议族:
	// 对于TCP/IP协议族而言,AF_INET用于IPv4,AF_INET6用于IPv6
	// type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务
	// protocol参数一般设置为0,表示使用默认协议
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	
	// sockaddr_in是TCP/IP协议族专用socket地址结构体,用于IPv4
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(2048);
	
	// 命名socket:int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen)
	// 在服务器程序中,我们要给socket命名这样客户端才能知道如何连接它,客户端则不需要命名socket
	// bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符
	// addrlen参数指出该socket地址的长度
	int name_fd = bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
	if(name_fd == -1){
		perror("bind error");
		return -1;
	}
	
	// 监听socket:int listen(int sockfd, int backlog)
	// backlog参数提示内核监听队列的最大长度,如果长度超过backlog,服务器将不受理新的客户连接
	name_fd = listen(sockfd, 10);
	
	// 接受连接:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
	// 从listen监听队列中接受一个连接
	// sockfd参数是执行过listen系统调用的监听socket
	// addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);
	int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
	
	// TCP数据读写:
	// ssize_t recv(int sockfd, void *buf, size_t len, int flags)
	// recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数通常设置为0
	// ssize_t send(int sockfd, const void *buf, size_t len, int flags)
	// send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小
	while(1){  // while循环可以多次接收客户端发送的数据
		char buffer[BUFFER_LENGTH] = {0};
		int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		// 服务器解析收到的内容然后才能确定发送给谁
		if(ret == 0){
			close(clientfd);
			break;
		}
		send(clientfd, buffer, ret, 0);
	}
	return 0;
}

客户端

#include <sys/socket.h> 
#include <netinet/in.h> 
#include <unistd.h> 
#include <stdio.h> 
#include <string.h>

#define BUFFER_LENGTH 1024

int main(){ 
	// 创建socket 
	int sockfd = socket(AF_INET, SOCK_STREAM, 0); 
	if (sockfd == -1){ 
		perror("socket error"); 
		return -1; 
	} 
	// 设置服务器地址信息 
	struct sockaddr_in serveraddr; 
	memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 
	serveraddr.sin_family = AF_INET; 
	serveraddr.sin_addr.s_addr = inet_addr("服务器IP地址"); // 服务器的IP地址 
	serveraddr.sin_port = htons(2048); // 服务器监听的端口号 
	
	// 连接服务器:int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen)
	// serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度
	// 一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信
	int connectfd = connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr)); 
	if (connectfd == -1){ 
		perror("connect error"); 
		close(sockfd); 
		return -1; 
	} 
	
	// 发送数据给服务器 
	char sendData[BUFFER_LENGTH] = "Hello, Server!"; 
	send(sockfd, sendData, strlen(sendData), 0); 
	// 接收服务器的响应 
	char recvData[BUFFER_LENGTH] = {0}; 
	recv(sockfd, recvData, BUFFER_LENGTH, 0); 
	printf("Received from server: %s\n", recvData); 
	// 关闭连接 
	close(sockfd); 
	return 0; 
}

2. 单服务端多客户端TCP通信

2.1 函数实现多线程

  • 优点:逻辑简单
  • 缺点:无法解决高并发问题

服务端

#include <pthread.h>

void *client_thread(void *arg){
	int clientfd = *(int*)arg;
	while(1){   // while循环可以多次接收客户端发送的数据
		char buffer[BUFFER_LENGTH] = {0};
		int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		if(ret == 0){
			close(clientfd);
			break;
		}
		send(clientfd, buffer, ret, 0);
	}
}

int main(){
	...
	int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
	
	pthread_t threadid;
	pthread_create(&threadid, NULL, client_thread, &clientfd);
	return 0;
}

2.2 select 实现多线程

  • 多线程客户端的做法是一个客户端启动一个线程,而 select( ) 把所有的连接都放在一个线程里
  • select(maxfd, rset, wset, eset, timeout)
    • maxfd:最大的fd(fd是依次增加的int值。其中0是stdin,1是stdout,2是stderror)
    • rset:可读集合、wset:可写集合、eset:出错集合
    • timeout:超时(多次时间轮询一次)
  • select处理的最大int长度是1024,是在内核里确定的,如果io更大则会提示数组越界
int main(){
	...
	name_fd = listen(sockfd, 10);
	
	fd_set rfds, rset;
	FD_ZERO(&rfds);  // 初始化文件描述符集 rfds,并将其中的所有文件描述符清零
	FD_SET(sockfd, &rfds); // 将监听的 sockfd 添加到文件描述符集 rfds
	int maxfd = sockfd;
	int clientfd = 0;
	while(1){
		rset = rfds;
		// 使用 select 函数监视多个文件描述符的可读性,它返回准备好读取的文件描述符数
		int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		// 检查监听套接字是否准备好读取,表示有新的连接请求
		if(FD_ISSET(sockfd, &rset)){
			clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept: %d\n", clientfd);
			FD_SET(clientfd, &rfds);
			if (clientfd > maxfd) maxfd = clientfd;
			if (--nready == 0) continue;
		}
		int i = 0;
		for (i = sockfd+1; i <= maxfd; i++){
			if (FD_ISSET(i, &rset)){
				char buffer[BUFFER_LENGTH] = {0};
				int ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if(ret == 0){
					FD_CLR(i, &rfds);
					close(i);
					break;
				}
				send(i, buffer, ret, 0);
			}
		}
	}
	return 0;
}
  • select 的缺点:
    • 参数较多,每个参数需要单独去维护
    • 每次需要遍历io集合,从而返回就绪集合
    • 每次都要把待检测的io集合copy进内核,对性能有影响(应用层的多个io要一一对应内核中的bit_set数组)
    • 对io的数量有限制(最多1024个io,在内核中设定的)

2.3 poll 实现多线程

  • poll只是select在参数上的一个优化,减少了 一些不必要参数的复制,他们底层实现是一样,都是基于select设计的
#include <sys/poll.h>

#define POLL_SIZE 1024

int main(){
	...
	name_fd = listen(sockfd, 10);
	
	struct pollfd fds[POLL_SIZE] = {0};
	fds[sockfd].fd = sockfd;
	fds[sockfd].events = POLLIN;  // 事件类型为 POLLIN,表示有数据可读
	int maxfd = sockfd;
	int clientfd = 0;
	while(1){
		int nready = poll(fds, maxfd+1, -1);
		// 检查监听套接字是否准备好读取,表示有新的连接请求
		if(fds[sockfd].revents & POLLIN){
			clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
			printf("accept: %d\n", clientfd);
			fds[clientfd].fd = clientfd;
			fds[clientfd].events = POLLIN;
			if (clientfd > maxfd) maxfd = clientfd;
			if (--nready == 0) continue;
		}
		int i = 0;
		for (i = 0; i <= maxfd; i++) {
			if (fds[i].revents & POLLIN) {
				char buffer[BUFFER_LENGTH] = {0};
				int ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret == 0) {
					fds[i].fd = -1;
					fds[i].events = 0;
					close(i);
					break; 
				}
				printf("ret: %d, buffer: %s\n", ret, buffer);
				send(i, buffer, ret, 0);
			}
		}
	}
	return 0;
}

2.4 epoll 实现多线程

#include <sys/poll.h>

#define POLL_SIZE 1024

int main(){
	...
	name_fd = listen(sockfd, 10);
	
	int epfd = epoll_create(1); // 参数 1 是一个暂时无意义的值,大于0即可
	
	struct epoll_event ev;
	ev.events = EPOLLIN; // 设置要监听的事件类型,EPOLLIN,表示有数据可读
	ev.data.fd = sockfd;
	
	epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 将监听套接字添加到 epoll 实例中进行监听
	
	struct epoll_event events[1024] = {0}; // 创建一个用于存储触发事件的 epoll 事件数组
	while(1){
		int nready = epoll_wait(epfd, events, 1024, -1);
		if(nready < 0) continue;
		
		int i = 0;
		for(i=0; i<nready; ++i){
			int connfd = events[i].data.fd; // 获取触发事件的文件描述符
			if(sockfd == connfd){
				struct sockaddr_in clientaddr;
				socklen_t len = sizeof(clientaddr);
				
				int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
				if(clientfd <=0) continue;
				
				// 修改新客户端文件描述符的监听事件为EPOLLIN(有数据可读)和EPOLLET(设置边缘触发)
				ev.events = EPOLLIN | EPOLLET;
				ev.data.fd = clientfd; 
				epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
			}
			else if(events[i].events & EPOLLIN){
				char buffer[10] = {0};
				int count = recv(connfd, buffer, 10, 0);
				if(count == 0){
					printf("close\n");
					epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
					close(i);
					continue;
				}
				send(connfd, buffer, count, 0);
			}
		}
	}
}
  • 水平触发LT:io传过来40个字节数据,服务端一次最多接收10个,可以一直接收直到io数据传完
  • 边沿触发ET:服务端只接收一次10个字节的数据,每send一次接收一次

2.4 事件驱动 reactor 实现多线程

  • select( )、poll( )、epoll( )都是针对io处理的,io越多,代码越冗杂
  • reactor是针对事件 EPOLLIN / EPOLLOUT 处理的,即需要读一个事件如何做,需要写一个事件如何做
if(events & EPOLLIN){ ... }
else if(events & EPOLLOUT){ ... }
  • reactor把整个网络io封装只留下两个接口,一个rbuffer,一个wbuffer。reactor只关注rbuffer是否有数据,并且把wbuffer中的数据发送即可,rbuffer针对EPOLLIN,wbuffer针对EPOLLOUT
    • listenfd 触发 EPOLLIN 事件时,执行 accept_cb( )
    • clientfd 触发 EPOLLIN 事件时,执行 recv_cb( )
    • listenfd 触发 EPOLLOUT 事件时,执行 send_cb( )
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <sys/time.h>

#define BUFFER_LENGTH 512

typedef int (*RCALLBACK)(int fd);
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);

struct conn_item{
	int fd;
	char rbuffer[BUFFER_LENGTH];
	int rlen;
	char wbuffer[BUFFER_LENGTH];
	int wlen;
	union{
		RCALLBACK accept_callback;
		RCALLBACK recv_callback;
	} recv_t;
	RCALLBACK send_callback;
};

int epfd = 0;
struct conn_item connlist[1048576] = {0};

int set_event(int fd, int event, int flag){
	// flag = 1 add, flag = 0 mod
	if(flag){     
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
	}
	else{
		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
	}
}

int accept_cb(int fd){
	struct sockaddr_in clientaddr;
	socklen_t len = sizeof(clientaddr);
	
	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
	if (clientfd < 0) {
		return -1;
	}
	set_event(clientfd, EPOLLIN, 1);
	connlist[clientfd].fd = clientfd;
	memset(connlist[clientfd].rbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].rlen = 0;
	memset(connlist[clientfd].wbuffer, 0, BUFFER_LENGTH);
	connlist[clientfd].wlen = 0;
	
	connlist[clientfd].recv_t.recv_callback = recv_cb;
	connlist[clientfd].send_callback = send_cb;
	return clientfd;
}

int recv_cb(int fd){
	char *buffer = connlist[fd].rbuffer;
	int idx = connlist[fd].rlen;
	
	int count = recv(fd, buffer+idx, BUFFER_LENGTH-idx, 0);
	if (count == 0) {
		printf("disconnect\n");
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);		
		close(fd);
		return -1;
	}
	connlist[fd].rlen += count;
	memcpy(connlist[fd].wbuffer, connlist[fd].rbuffer, connlist[fd].rlen);
	connlist[fd].wlen = connlist[fd].rlen;
	connlist[fd].rlen -= connlist[fd].rlen;
	set_event(fd, EPOLLOUT, 0);
	return count;
}

int send_cb(int fd){
	char *buffer = connlist[fd].wbuffer;
	int idx = connlist[fd].wlen;
	int count = send(fd, buffer, idx, 0);
	set_event(fd, EPOLLIN, 0);
	return count;
}

int init_server(unsigned short port){
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(port);
	int name_fd = bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
	if (name_fd == -1){
		perror("bind error");
		return -1;
	}
	name_fd = listen(sockfd, 10);
	return sockfd;
}

int main(){
	int port_count = 20;
	unsigned short port = 2048;
	int i = 0;
	int epfd = epoll_create(1);
	for(i = 0; i < port_count; ++i){
		int sockfd = init_server(port + i);  // 2048, 2049, 2050, 2051 ... 2057
		connlist[sockfd].fd = sockfd;
		connlist[sockfd].recv_t.accept_callback = accept_cb;
		set_event(sockfd, EPOLLIN, 1);
	}
	struct epoll_event events[1024] = {0};
	while(1){
		int nready = epoll_wait(epfd, events, 1024, -1); 
		int i = 0;
		for (i = 0; i < nready; i ++) {
			int connfd = events[i].data.fd;
			if (events[i].events & EPOLLIN) { 
				int count = connlist[connfd].recv_t.recv_callback(connfd);
				printf("recv count: %d <-- buffer: %s\n", count, connlist[connfd].rbuffer);
			} 
			else if (events[i].events & EPOLLOUT) { 
				// printf("send --> buffer: %s\n",  connlist[connfd].wbuffer);
				int count = connlist[connfd].send_callback(connfd);
			}
		}
	}
	return 0;
}

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值