C++ 之网络编程基础复习总结

news2024/9/21 0:35:22

基础

IP 地址可以在网络环境中唯一标识一台主机。

端口号可以在主机中唯一标识一个进程。

所以在网络环境中唯一标识一个进程可以使用 IP 地址与端口号 Port 。

字节序

TCP/IP协议规定,网络数据流应采用大端字节序

大端:低地址存高位,高地址存低位;

小端:低地址存低位,高地址存高位(x86采用小端存储)。

网络字节序,就是在网络中进行传输的字节序列,采用的是大端法。

主机字节序,就是本地计算机中存储数据采用的字节序列,采用的是小端法。

相关 API 函数

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//h = host n = network l = long s = short
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);


#include <arpa/inet.h>
//点分十进制字符串转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
//网络字节序转换为点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);


#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> //包含了<netinet/in.h>,后者包含了<sys/socket.h>

typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

//将cp所指C字符串转换成一个32位的网络字节序二进制值,并通过inp指针来存储,成功返回1,失败返回0
int inet_aton(const char *cp, struct in_addr *inp);

//将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串,由该函数的返回值所指向的
//字符串驻留在静态内存中,这意味着该函数是不可重入的
char *inet_ntoa(struct in_addr in);

//inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char
//*cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-
//1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;
//返回值为32位的网络字节序二进制
in_addr_t inet_addr(const char *cp);//ok
in_addr_t inet_network(const char *cp);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);


#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

常用结构体

man 7 ip 可以查看相应的结构体,也可以使用命令sudo grep -rn "struct sockaddr_in {" /usr 进行搜索。

在这里插入图片描述

struct sockaddr
{
	sa_family_t sa_family; /* address family, AF_xxx */
	char sa_data[14]; /* 14 bytes of protocol address */
};

struct sockaddr_in
{
	sa_family_t sin_family; /* address family: AF_INET */
	in_port_t sin_port; /* port in network byte order */
	struct in_addr sin_addr; /* internet address */
};

/* Internet address. */
struct in_addr
{
	uint32_t s_addr; /* address in network byte order */
};

IPv4IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,IPv6 地址使用 sockaddr_in6 结构体表示。UNIX Domain Socket 的地址格式定义在 sys/un.h 中,使用 sockaddr_un 结构体表示。所有的地址类型分别定义为常数 AF_INETAF_INET6AF_UNIX

struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6/AF_UNIX;
addr.sin_port = htons/ntohs;
addr.sin_addr.s_addr = htonl/ntohl/inet_pton/inet_ntop

网络编程相关函数

socket 函数

创建套接字:

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

//创建套接字函数
int socket(int domain, int type, int protocol);

domain:AF_INET/AF_INET6/AF_UNIX
type:SOCK_STREAM/SOCK_DGRAM 前者默认是TCP,后者默认是UDP
protocol:0表示使用默认协议
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno

bind 函数

因为服务器程序所监听的网络地址与端口号是固定不变的,所以需要使用bind函数进行绑定。bind 函数将 sockfd 与 addr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。

绑定 IP 与端口号:

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//绑定服务的端口号与IP地址
sockfd:上面socket创建的套接字
addr:所要绑定的ip地址与端口号
addrlen:前面addr结构体的长度
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno

listen 函数

用来指定监听上限数值(允许同时多少个客户端与服务器建立连接),指定最大同时发起连接数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:socket创建的文件描述符
backlog:排队建立3次握手队列和刚刚建立3次握手队列的连接数和。

可以使用命令进行最大发起连接数限定值的查看:

cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1

accept 函数

接收连接请求的函数,阻塞等待客户端发起连接

如果客户端还没有来得及连接,此时 accept 函数会处于阻塞状态。

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:socket创建的文件描述符
addr:传出参数,返回连接客户端地址信息,包含IP地址与端口号
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构
体的大小。
//函数返回值
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

connect 函数

客户端调用该函数,连接到服务器上。

发起连接:

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd:是客户端自己使用socket得到的文件描述符。
addr:传入参数,指定服务器端地址信息,包含IP地址与端口号
addrlen:传入参数,传入sizeof(addr)大小
返回值:成功返回0,失败返回-1,设置errno

客户端需要调用 connect 连接服务器,connect 和 bind 的采纳数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。

close 函数

关闭套接字创建的文件描述符。

#include <unistd.h>

int close(int fd);

客户端其实也是需要 bind 端口号与 IP 地址,如果没有显示绑定的话,操作系统会自动分配一个 IP 地址与端口号。但是服务器是不能不使用 bind 函数,让操作系统随机分配 IP 地址与端口号,这样的话客户端就不知道服务器的 IP 地址与端口号,就不知道怎么连接到服务器上了,也不知道连接到那个服务器上。

本地随机的有效数字类型的 IP,INADDR_ANY

INADDR_ANY解析:转换过来就是 0.0.0.0,泛指本机的意思,表示本机的所有IP,因为有些电脑不止一块网卡,如果某个应用程序只监听某个端口,那么其他端口过来的数据就接收不了。

网络编程代码

逻辑示例图

在这里插入图片描述

端口复用

让同一个端口可以进行重复使用,不至于等待 2MSL的时间:

#include <sys/types.h>
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void* optval, 
	socklen_t* optlen);

int setsockopt(int sockfd, int level, int optname,
	const void* optval, socklen_t optlen);

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

服务器端源码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666

int main()
{
	int sfd, cfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	char buf[BUFSIZ], clie_IP[BUFSIZ];
	int nByte, idx;
	sfd = socket(AF_INET, SOCK_STREAM, 0);
	int opt = 1;
	setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//允许端口复用
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	listen(sfd, 128);
	clie_addr_len = sizeof(clie_addr);
	cfd = accept(sfd, (struct sockaddr*)&clie_addr, &clie_addr_len);
	printf("client IP: %s, port: %d\n",
		inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP,
			sizeof(clie_IP)),
		ntohs(clie_addr.sin_port));
	while (1)
	{
		nByte = read(cfd, buf, sizeof(buf));
		for (idx = 0; idx < nByte; ++idx)
		{
			buf[idx] = toupper(buf[idx]);
		}
		write(cfd, buf, nByte);
	}
	close(sfd);
	close(cfd);
	return 0;
}

客户端源码

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

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

read 返回值

1、大于0,实际读到的字节数,并且buf=1024

如果read读到的数据的长度等于buf,返回的就是1024
如果read读到的数据长度小于buf,那就是小于1024的数值。

2、返回值为0,数据读完(读到文件、管道、socket末尾 —对端关闭)

3、返回值为-1,表明出现异常

errno == EINTR 说明被信号中断 所以需要重启或者退出;
errno == EAGAIN(EWOULDBLOCK)非阻塞方式读,并且没有数据;
其他值的出现表示出现错误使用 perror 打印然后 exit 退出

readn/writen 函数的封装

因为以太网帧一次只能传送 1500 字节的数据,所以使用 read 函数一次最多只能读到 1500 字节,就返回退出。

ssize_t readn(int fd, void* vptr, size_t n)
{
	size_t nleft;//usigned int剩余未读取的字节数
	size_t nread;//int 实际读到的字节数
	char* ptr;
	nleft = n;//n未读取字节数
	ptr = vptr;
	while (nleft > 0)
	{
		if ((nread = read(fd, ptr, nleft)) < 0)
		{
			if (errno == EINTR)
			{
				nread = 0;
			}
			else
			{
				return -1;
			}
		}
		else if (0 == nread)
		{
			break;
		}
		nleft -= nread;
		ptr += nread;
	}
	return (n - nleft);
}
ssize_t writen(int fd, const void* vptr, size_t n)
{
	size_t nleft;
	size_t nwritten;
	const char* ptr;
	nleft = n;
	ptr = vptr;
	while (nleft > 0)
	{
		if ((nwritten = write(fd, ptr, nleft)) <= 0)
		{
			if (nwritten < 0 && errno == EINTR)
			{
				nwritten = 0;
			}
			else
			{
				return -1;
			}
		}
		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

IO多路复用

概念与原理图

多进程与多线程并发服务器,不经常使用这种作为大型服务器开发的原因是,所有的监听与访问请求都由服务器操作

可以使用多路IO转接服务器(也叫多任务IO服务器),思想:不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件

在这里插入图片描述

select

接口解析

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
	struct timeval* timeout);
	
nfds:监控的文件描述符集里最大文件描述符 + 1,因为此参数会告诉内核检测前多少个文件描述符的状态。
readfs / writes / exceptfds : 监控有读数据 / 写数据 / 异常发生到达文件描述符集合,三个都是传入传出参数。
timeout : 定时阻塞监控时间,3种情况:
1NULL,永远等下去
2、设置timeval,等待固定时间
3、设置timeval里时间均为0,检查描述字后立即返回,轮询。
fd_set:本质是个位图。
struct timeval
{
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};
返回值:
成功:所监听的所有的监听集合中,满足条件的总数。
失败:返回 -1.
void FD_ZERO(fd_set *set);//将set清空为0
void FD_SET(int fd, fd_set *set);//将fd设置到set集合中
void FD_CLR(int fd, fd_set *set);//将fd从set中清除出去
int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中

优缺点

1、文件描述符上限(1024),同时监听的文件描述符1024个,历史原因,不好修改,除非重新编译Linux内核。

2、当监听的文件描述符个数比较稀疏的时候(比如3, 600, 1023),循环判断比较麻烦,所以需要自定义数据结构:数组。

3、监听集合与满足监听条件的集合是同一个,需要将原有集合保存。

代码实现(C语言)

server 端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_PORT 8888

int main()
{
	int listenfd, connfd, sockfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	int ret, maxfd, maxi, i, j, nready, nByte;
	fd_set rset, allset;
	int client[FD_SETSIZE];
	char buf[BUFSIZ], str[BUFSIZ];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr)); 
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	maxfd = listenfd;
	maxi = -1;
	for (i = 0; i < FD_SETSIZE; ++i)
	{
		client[i] = -1;
	}
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);
	while (1)
	{
		rset = allset;
		nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
		if (nready < 0)
		{
			perror("select error");
			exit(-1);
		}
		if (FD_ISSET(listenfd, &rset))
		{
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
				&clie_addr_len);
			if (-1 == connfd)
			{
				perror("accept error");
				exit(-1);
			}
			printf("receive from %s from port %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr, str,
					sizeof(str)),
				ntohs(clie_addr.sin_port));
			for (i = 0; i < FD_SETSIZE; ++i)
			{
				if (client[i] < 0)
				{
					client[i] = connfd;
					break;
				}
			}
			if (i == FD_SETSIZE)
			{
				fputs("too many clients\n", stderr);
				exit(1);
			}
			FD_SET(connfd, &allset);
			if (connfd > maxfd)
			{
				maxfd = connfd;
			}
			if (i > maxi)
			{
				maxi = i;
			}
			if (--nready == 0)
			{
				continue;
			}
		}
		for (i = 0; i <= maxi; ++i)
		{
			if ((sockfd = client[i]) < 0)
			{
				continue;
			}
			if (FD_ISSET(sockfd, &rset))
			{
				if ((nByte = read(sockfd, buf, sizeof(buf))) == 0)
				{
					close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				}
				else if (nByte > 0)
				{
					for (j = 0; j < nByte; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, nByte);
					write(STDOUT_FILENO, buf, nByte);
				}
				if (--nready == 0)
				{
					break;
				}
			}
		}
	}
	close(listenfd); 
	close(connfd);
	return 0;
}

client 端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

poll

接口解析

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd
{
	int fd; /* file descriptor */
	short events; /* requested events */
	short revents; /* returned events */
};

fds:文件描述符数组。
events:POLLIN/POLLOUT/POLLERR
nfds:监控数组中有多少文件描述符需要被监控。
timeout 毫秒级等待:
	-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
	0:立即返回,不阻塞进程
	>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值。
函数返回值:满足监听条件的文件描述符的数目。

优缺点

优点:

1、突破文件描述符1024的上限
2、监听与返回的集合分离
3、搜索范围变小(已经知道是哪几个数组)

缺点:

1、监听1000个文件描述符,但是只有3个满足条件,这样也需要全部遍历,效率依旧低。
2、cat /proc/sys/fs/file-max 查看一个进程可以打开的文件描述符的上限数。
3、sudo vi /etc/security/limits.conf。在文件尾部写入以下配置,soft 软限制,hard 硬限制。

soft nofile 65536
hard nofile 100000

代码实现(C语言)

server 端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>

#define SERV_PORT 8888
#define OPEN_MAX 1024

int main()
{
	int i, j, n, maxi;
	int nready, ret;
	int listenfd, connfd, sockfd;
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	struct pollfd client[OPEN_MAX];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);//本地字节序port与ip都要转换为网络字节序
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//因为要在网络上传输
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr));
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	client[0].fd = listenfd;
	client[0].events = POLLIN;
	for (i = 1; i < OPEN_MAX; ++i)
	{
		client[i].fd = -1;//将数组初始化为-1
	}
	maxi = 0;
	while (1)
	{
		nready = poll(client, maxi + 1, -1);
		if (nready < 0)
		{
			perror("poll error");
			exit(-1);
		}
		if (client[0].revents & POLLIN)
		{
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
				&clie_addr_len);//立即连接,此时不会阻塞等
			if (-1 == connfd)
			{
				perror("accept error");
				exit(-1);
			}
			printf("received from %s at port %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, str,
					sizeof(str)),
				ntohs(clie_addr.sin_port));
			for (i = 1; i < OPEN_MAX; ++i)
			{
				if (client[i].fd < 0)//因为初始化为-1,所以在此作为判断条件
				{
					client[i].fd = connfd;
					break;//直接跳出,免得继续判断,浪费时间
				}
			}
			if (i == OPEN_MAX)//select监听的文件描述符有上限,最大只能监听1024个
			{
				fputs("too many clients\n", stderr);
				exit(1);
			}
			client[i].events = POLLIN;
			if (i > maxi)
			{
				maxi = i;//因为文件描述符有新增,导致自定义数组有变化,所以需要重新
				修改maxi的值
			}
			if (--nready == 0)//意思不明确
			{
				continue;
			}
		}
		for (i = 1; i <= maxi; ++i)
		{
			if ((sockfd = client[i].fd) < 0)
			{
				continue;
			}
			if (client[i].revents & POLLIN)
			{
				if ((n = read(sockfd, buf, sizeof(buf))) < 0)
				{
					if (errno == ECONNRESET)
					{
						printf("client[%d] abort connect\n", i);
						close(sockfd);
						client[i].fd = -1;
					}
					else
					{
						perror("read n = 0 error");
					}
				}
				else if (n > 0)
				{
					for (j = 0; j < n; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, n);
					write(STDOUT_FILENO, buf, n);
				}
				else
				{
					close(sockfd);
					client[i].fd = -1;
				}
				if (--nready == 0)
				{
					break;
				}
			}
		}
	}
	close(listenfd);
	close(connfd);
	return 0;
}

client 端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

epoll

接口解析

是Linux下IO多路复用接口select/poll的增强版本,能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要侦听的文件描述符集合,另一个原因是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历哪些被内核IO事件唤醒而加入Ready队列的描述符集合就行了

#include <sys/epoll.h>

int epoll_create(int size);

size:参数size用来告知内核监听的文件描述符的个数,与内存大小有关。

//控制某个epoll监控的文件描述符上的事件:注册、修改、删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epfd:epoll_create函数返回的值
op:EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL
fd:将哪个文件描述符以op的方式加在以epfd建立的树上
event:告诉内核需要监听的事情。

struct epoll_event
{
	uint32_t events;
	epoll_data_t data;
};

typedef union epoll_data
{
	void* ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

//等待所监控文件描述符上有事件的产生
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, 
				int timeout);
				
events:用来存内核得到事件的集合(这里是个传出参数)
maxevents:告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size
timeout:是超时时间
	-1:阻塞
	=0:立即返回,非阻塞
	>0:指定毫秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回 - 1

优缺点

优点:

1、文件描述符数目没有上限:通过 epoll_ctl() 来注册一个文件描述符,内核中使用红黑树的数据结构来
管理所有需要监控的文件描述符。

2、基于事件就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于 callback 的回调机制,迅速激活这个文件描述符,这样随着文件描述符数量的增加,也不会影响判定就绪的性能。

3、维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中,这样调用 epoll_wait 获取就绪文件描述符的时候,只要取队列中的元素即可,操作的时间复杂度恒为 O(1) 。

图解

在这里插入图片描述

类型区别

水平触发(level-triggered)

只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知;当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知LT模式支持阻塞和非阻塞两种方式。

epoll默认的模式是LT

边缘触发(edge-triggered)

当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知;当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。

两种类型区别

两者的区别在哪里呢?

水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次。

LT(level triggered) 是缺省的工作方式,并且同时支持 block 和 no-block socket.

在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的 select/poll 都是这种模型的代表.

在这里插入图片描述

当设置了边缘触发以后,以可读事件为例,对“有数据到来”这事件为触发。

在这里插入图片描述

select/poll/epoll 除了应用于 fd 外,像管道、文件也是可以的。

代码实现(C语言)

server端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_PORT 8888
#define OPEN_MAX 5000

int main()
{
	int listenfd, connfd, sockfd, epfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	int ret, i, j, nready, nByte;
	char buf[BUFSIZ], str[BUFSIZ];
	struct epoll_event evt, ep[OPEN_MAX];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr));
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	epfd = epoll_create(OPEN_MAX);
	if (-1 == epfd)
	{
		perror("epoll_create error");
		exit(-1);
	}
	evt.events = EPOLLIN;
	evt.data.fd = listenfd;
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &evt);
	if (-1 == ret)
	{
		perror("epoll_ctl error");
		exit(-1);
	}
	while (1)
	{
		nready = epoll_wait(epfd, ep, OPEN_MAX, -1);
		if (nready < 0)
		{
			perror("select error");
			exit(-1);
		}
		for (i = 0; i < nready; ++i)
		{
			if (!(ep[i].events & EPOLLIN))
			{
				continue;
			}
			if (ep[i].data.fd == listenfd)//如果是连接事件
			{
				clie_addr_len = sizeof(clie_addr);
				connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
					&clie_addr_len);
				if (-1 == connfd)
				{
					perror("accept error");
					exit(-1);
				}
				printf("receive from %s from port %d\n",
					inet_ntop(AF_INET, &clie_addr.sin_addr, str,
						sizeof(str)),
					ntohs(clie_addr.sin_port));
				evt.events = EPOLLIN;
				evt.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt);
			}
			else //不是连接建立事件,而是读写事件(信息传递事件)
			{
				sockfd = ep[i].data.fd;
				nByte = read(sockfd, buf, sizeof(buf));
				if (nByte == 0)
				{
					ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
					if (-1 == ret)
					{
						perror("epoll_ctl error");
					}
					close(sockfd);
					printf("client[%d] closed connection\n", sockfd);
				}
				else if (nByte < 0)
				{
					perror("epoll_ctl error");
					ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
					if (-1 == ret)
					{
						perror("epoll_ctl error");
					}
					close(sockfd);
				}
				else
				{
					for (j = 0; j < nByte; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, nByte);
					write(STDOUT_FILENO, buf, nByte);
				}
			}
		}
	}
	close(listenfd);
	close(connfd);
	return 0;
}

client端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2049018.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FastCopy文件快速复制v5.7.15

软件介绍 FastCopy文件快速复制工具。Windows平台上最快的文件复制、删除软件&#xff01;功能强劲&#xff0c;性能优越&#xff01;它是源于日本的高效文件复制加速软件&#xff0c;支持拖拽操作&#xff0c;三种不同HDD模式&#xff1b;支持通配符&#xff0c;任务管理/命令…

XSS项目实战

目录 一、项目来源 二、实战操作 EASY 1 2 3 4 5 6 7 8 一、项目来源 XSS Game - Learning XSS Made Simple! | Created by PwnFunction 二、实战操作 EASY 1 1.Easy -1 2.题目要求及源码 Difficulty is Easy.Pop an alert(1337) on sandbox.pwnfunction.com.No …

基于STM32开发的智能植物浇水系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化土壤湿度检测与浇水控制显示与状态指示Wi-Fi通信与远程监控应用场景 家庭植物自动浇水农业智能灌溉系统常见问题及解决方案 常见问题解决方案结论 1. 引言 智能植物浇水系统通过集…

学习记录第二十九天

信号量————来描述可使用资源的个数 信号量&#xff08;Semaphore&#xff09;是一种用于控制多个进程或线程对共享资源访问的同步机制。在C语言中&#xff0c;通常我们会使用POSIX线程&#xff08;pthread&#xff09;库来实现信号量的操作 信号量有两个主要操作&#xf…

【学习总结】JVM篇

JVM JVM基础知识 主力机型 HotSpot VM HotSpot虚拟机时OpenJDK和OracleJDK中默认的Java虚拟机。它最初并非由Sun公司所开发&#xff0c;而是由一家名为“Longview Technologies”的小公司设计。Sun公司注意到这款虚拟机在即时编译等多个方面有着优秀的理念和实际成果&#…

【Redis】字符串数据类型深入解析与应用实践

目录 String 字符串常⻅命令计数命令其他命令命令⼩结内部编码典型使⽤场景 String 字符串 字符串类型是 Redis 最基础的数据类型&#xff0c;关于字符串需要特别注意&#xff1a;1&#xff09;⾸先 Redis 中所有的键的类型都是字符串类型&#xff0c;⽽且其他⼏种数据结构也都…

牙科就诊管理系统--论文pf

TOC springboot399牙科就诊管理系统--论文pf 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;…

企业考勤管理神器:9款精选软件推荐

本文将介绍9款考勤管理软件&#xff1a;Moka、超人HR、慧点、云易通、麦勤通、TeKard考勤管理系统、Toggl Track、ZoomShift、Kronos Workforce Ready。 选择合适的考勤管理软件对企业来说可不是件小事。面对市场上琳琅满目的工具&#xff0c;选错了不仅浪费时间和金钱&#xf…

【二分查找】--- 初阶题目赏析

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Joureny 上篇我们讲解了关于二分的朴素模板和边界模板&#xff0c;本篇博客我们试着运用这些模板。 &#x1f3e0; 搜索插入位置 &#x1f4cc; 题目…

如何使用和配置 AWS CLI 环境变量?

欢迎来到雲闪世界。环境变量在配置和保护应用程序方面起着至关重要的作用&#xff0c;在使用 AWS CLI&#xff08;命令行界面&#xff09;时&#xff0c;它们的使用尤其重要。在这篇博客文章中&#xff0c;我们将深入探讨环境变量的世界&#xff0c;探索它们的用途、它们在 AWS…

【Python】OBS 脚本

文章目录 一、指定python解释器和脚本二、特殊函数名三、obspython API四、 实际应用示例(mkv转mp4封装后自动删除mkv)OBS Studio是一个流行的开源软件,用于视频录制和直播。除了其内置功能外,OBS还支持通过Python脚本(以及lua脚本)进行扩展,允许用户自定义和自动化各种…

JS模块化总结 | CommonJS、ES6

BV13W42197jR 个人笔记 目录 JS模块化基础知识1. 概述1.1 什么是模块化1.2 为什么需要模块化? 2 模块化规范3 导入&导出4 CommonJS规范4.1 初步体验4.2 导出数据4.3 导入数据4.4 扩展理解4.5 浏览器端运行 5 ES6模块化规范5.1 初步体验5.2 Node中运行ES65.3 导出数据①分别…

C++ 设计模式——工厂方法模式

工厂方法模式 工厂方法模式主要组成部分代码实现工厂方法模式模式的 UML 图工厂方法模式 UML 图解析优点和缺点适用场景 工厂方法模式 工厂方法模式是一种创建型设计模式&#xff0c;它通过定义一个接口用于创建对象&#xff0c;但由子类决定实例化哪个类。与简单工厂模式不同…

笔记 5 : 彭老师课本第 5 章 , 开始代码编程

&#xff08;49&#xff09;大纲&#xff1a; &#xff08;50&#xff09; 系统&#xff1a; &#xff08;51&#xff09; 学习路线&#xff1a; &#xff08;52&#xff09;该款 soc 的内存布局&#xff1a; 细化的 SFR 地址空间 &#xff1a; &#xff08;53&#xff09; soc…

IMX8M核心板偶发系统启动失败排查实录

一. 基本情况介绍 1. 硬件方案组成 产品中采用的是IMX8M核心板的方案&#xff0c;如图1所示是产品的硬件系统整体组成示意图。 图1 产品硬件组成框图 2. 问题描述 今年5月份有一台设备出现系统偶发启动失败&#xff0c;uboot阶段都无法通过&#xff0c;表面现象是显示屏黑屏…

Android持久化技术—SharedPreferences存储

文章目录 Android持久化技术—SharedPreferences存储将数据存储到SharedPreferences中Context类中的getSharedPreferences()方法Activity类中的getPreferences()方法PreferenceManager类中的getDefaultSharedPreferences()方法 从SharedPreferences中读取数据 Android持久化技术…

【自动驾驶】ROS远程节点的分布式通信

目录 固定IP的设置将IP地址改成手动配置文件修改配置主从机的~/.bashrc 文件配置主机的 IP 地址配置从机IP ROS是一个分布式计算环境。一个运行中的ROS系统可以包含分布在多台计算机上多个节点。根据系统的配置方式&#xff0c;任何节点可能随时需要与任何其他节点进行通信。 …

XSS靶场————XSS.pwnfunction

目录 第一关&#xff1a;Ma Spaghet! 第二关&#xff1a;Jefff 第三关&#xff1a;Ugandan Knuckles​编辑 第四关&#xff1a;Ricardo Milos ​编辑 第五关&#xff1a;Ah Thats Hawt​编辑 第六关&#xff1a;Ligma 第七关&#xff1a;Mafia​编辑 第八关&#xff1a…

交易系统JVM内存优化

背景 新交易系统上线以后&#xff0c;业务指标&#xff08;成单率&#xff09;和系统指标&#xff08;CPU、QPS、JVM内存&#xff09;是我们重点关注的指标。 CPU较高&#xff1a;可以通过Arthas等工具查看繁忙线程的堆栈信息&#xff0c;定位具体的代码&#xff0c;具体分析…

谷歌浏览器字体模糊不清怎么办

许多小伙伴在使用谷歌浏览器时&#xff0c;可能都遇见过字体模糊不清的情况&#xff0c;这将对我们的浏览体验大打折扣。为了解决这以问题&#xff0c;本文将为大家带来谷歌浏览器字体模糊不清的解决方法&#xff0c;帮助你享受到更清晰舒适的阅读环境。&#xff08;本文由chro…