31_网络

网络

网络基础

​ 怎么解决通信设备间的通信问题?解决这个问题,各设备就必须要使用同一套通信协议,才能互相理解对方“说的话”,目前在互联网中这个一直被我们使用的协议叫TCP/IP协议簇,简称TCP/IP。其中TCP是Transmission Control Protocol的简称,它是一种面向连接的、可靠的、基于字节流的传输层通信协议,IP是Internet Protocol的简称,它的任务仅仅是根据源主机和目的主机的地址来传送数据。

TCP/IP协议分为4层

image-20240408120612104

IP地址网段和分类

image-20240408120631305

4层架构在RFC 1122中描述的不同层数据的封装

image-20240408120716997

socket网络编程

​ linux中的网络编程通过socket接口实现。Socket既是一种特殊的IO,它也是一种文件描述符。一个完整的Socket 都有一个相关描述{协议,本地地址,本地端口,远程地址,远程端口};每一个Socket 有一个本地的唯一Socket 号,由操作系统分配。
​ TCP把连接作为最基本的对象,每一条TCP连接都有两个端点,这种断点我们叫作套接字(socket),它的定义为端口号拼接到IP地址即构成了套接字,例如,若IP地址为192.168.1.100 而端口号为80,那么得到的套接字为192.168.1.100:80。

套接字的三种类型:

  • 流式套接字(SOCK_STREAM)
    可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP 保证了数据传输的正确性和顺序
    性。

  • 数据报文套接字(SOCK_DGRAM)

    定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。使用数据报协议UDP协议。

  • 原始套接字
    原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。

基于数据流的socket编程流程

image-20240408120927462

基于数据报文的编程流程

image-20240408121017925

socket系列函数使用说明

struct sockaddr 和 struct sockaddr_in

这两个结构体用来处理网络通信的地址。

sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了,如下:

/* POSIX.1g specifies this type name for the `sa_family' member.  */
typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc.  */

#define	__SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family


/* Structure describing a generic socket address.  */
struct sockaddr
{
 __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
 char sa_data[14];		/* Address data.  */
};
struct sockaddr {  
  sa_family_t sin_family;//地址族
    char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息               
}; 

*sockaddr_in*在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:

这里写图片描述

/* Structure describing an Internet socket address.  */
struct sockaddr_in
{
 __SOCKADDR_COMMON (sin_);
 in_port_t sin_port;			/* Port number.  */
 struct in_addr sin_addr;		/* Internet address.  */

 /* Pad to size of `struct sockaddr'.  */
 unsigned char sin_zero[sizeof (struct sockaddr)
			   - __SOCKADDR_COMMON_SIZE
			   - sizeof (in_port_t)
			   - sizeof (struct in_addr)];
  };

举例:

img

  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
  4. 端口号为SERV_PORT, 我们定义为8080;

socket

功能: 创建socket套接字

image-20240409174749949

bind

img

listen

image-20240408123615089

accept

image-20240408123634232

connect

image-20240408123648866

htons/htonl

#include <arpa/inet.h> 

uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);

这两个函数是为了解决大小端问题而生的。

功能:将一个uint16_t或者uint32_t类型数值转换为网络字节序的数值,即大端模式(big-endian)(在主机本身就使用大端字节序时,函数通常被定义为空宏)。

可以通过其英文记忆两个函数:

  • h(=host “主机”字节序的意思)
  • to:转换
  • n(=network "网络"字节序的意思)
  • l/s(l=long 32位,s=short 16位)

扩展: 大端模式(TCP / IP网络字节顺序)、小端模式是什么?

举个例子:假定你的port是 0x1234,

在x86电脑上,0x1234放到内存中实际是:addr addr+1  0x34 0x12 (我们常用的 x86 CPU (intel, AMD) 电脑是小端模式( little-endian),也就是整数的低位字节放在内存的低字节处。)

而在网络字节序里 这个port放到内存中就应该显示成  addr addr+1 0x12 0x34(而TCP / IP网络字节顺序是大端模式( big-endian),也就是整数的高位字节存放在内存的低地址处。)

htons 的用处就是把实际内存中的整数存放方式调整成“网络字节序”的方式。

inet_pton

#include <arpa/inet.h> 

int inet_pton(int af, const char *src, void *dst);

是为了解决人可读和机器执行的冲突而生的。

点分十进制表示的字符串形式(人容易读)转换成二进制 Ipv4 或Ipv6 地址(机器容易理解)

我是这样记忆这个函数的:

  • p = people read 意思是人类可读的。
  • to 转变
  • n = network read 意思是网络识别的

函数原型:

int inet_pton(int af, const char *src, void *dst);

参数:

​ af: 地址族 (Address Family)。AF_INET 或AF_INET6,AF_INET 表示待转换的Ipv4地址,AF_INET6 表示待转换的是Ipv6 地址;

​ src: 指向字符串形式的 IP 地址的指针。点分十进制表示的字符串

​ dst: 一个指向存储转换后的二进制地址的缓冲区的指针。对于 IPv4,这应该是一个指向 struct in_addr 的指针;对于 IPv6,这应该是一个指向 struct in6_addr 的指针。

返回值:

  • 成功时返回1。

  • 如果输入地址不是有效的表现形式,返回0。

  • 出错时返回-1,并设置 errno 为具体的错误代码。

示例:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    const char *ip_str = "192.168.1.1";
    struct in_addr ip_addr;  // for IPv4

    if (inet_pton(AF_INET, ip_str, &ip_addr) <= 0) {
        perror("inet_pton");
        return 1;
    }

    printf("Binary representation of IP: %u\n", ip_addr.s_addr);
    return 0;
}

inet_ntop

#include <arpa/inet.h> 

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

ntop()参数:基本与pton()相同,src 和 dst 相反而已。

ntop()返回值:

inet_ntop()在成功时会返回dst 指针。如果size 的值太小了,那么将会返回NULL 并将errno 设置为ENOSPC。

inet_ntoa

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr addr );

功能:是将一个32位大端网络字节序整数转换为点分十进制的ipv4的IP地址。
参数:
addr: 属于sockaddr_in结构体的结构体in_addr地址
返回值:存放转化结果的首地址,char*指针,要提前分配空间。失败时返回-1。

setsockopt

功能介绍:

setsockopt是用来为网络套接字设置选项值,比如:允许重用地址、网络超时等;在Linux下和Windows下均有该函数,但是使用略有不同;很多语言也支持或者封装了该接口

函数原型:

#include <sys/types.h>          /* See NOTES */
#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);

参数介绍:

  • sockfd:网络套接字

  • level:协议层,整个网络协议中存在很多层,指定由哪一层解析;通常是SOL_SOCKET,也有IPPROTO_IP/IPPROTO_TCP

  • optname:需要操作的选项,比如SO_RCVTIMEO接受超时

  • optval:设置的选项值,类型不定;比如SO_REUSERADDR设置地址重用,那么只需要传入一个int指针即可

  • optlen:选项值的长度

选项介绍:

  • SO_REUSERADDR:允许本地地址和端口重用,参数为int类型;在Linux下主要作用是:关闭套接字监听后其地址很快能被另一个套接字使用;如果不设置的话,同一个程序绑定同一个端口,关闭后立马再次运行可能会遭遇绑定失败

  • SO_RCVTIMEO:为接受数据的系统调用设置超时,包括:recvrecvfromaccept;若超时, 返回-1,并设置errno为EAGAIN或EWOULDBLOCK;网络套接字在运行中是会出现阻塞的情况,设置该值可能提升程序健壮性。

简单示例——client端:

#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
    char szBuf[1024];
    struct sockaddr_in addr;
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == fd) {
        perror("socket");
        return -1;
	} 
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) {
        perror("connect");
        close(fd);
        return -1;
	}
    while(1) {
        scanf("%s", szBuf);
        write(fd, szBuf, strlen(szBuf)+1);
        memset(szBuf, 0, sizeof(szBuf));
        read(fd, szBuf, sizeof(szBuf));
        printf("recv from server:%s\n", szBuf);
    } 
    return 0;
}

简单示例——server端:

#include <stdio.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main()
{
    char szBuf[1024];
    struct sockaddr_in addr;
    struct sockaddr_in client_addr;
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == fd) {
        perror("socket");
        return -1;
    } 
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(fd, 20);
    socklen_t nSockLen;
    while (1) {
        int nConnFd = accept(fd, (struct sockaddr *)&client_addr, &nSockLen);
        while(1) {
            int nRet = read(nConnFd, szBuf, sizeof(szBuf));
            if (nRet < 0) {
                perror("read");
                close(fd);
                break;
            } 
            write(nConnFd, szBuf, nRet);
            printf("recv from client:%s\n", szBuf);
        }
    } 
    close(nConnFd);
    close(fd);
    return 0;
}

聊天室示例代码——服务端

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

#define CHAT_DEBUG 1
#if CHAT_DEBUG
#define DBG(x...) printf(x)
#else
#define DBG(x...)
#endif

void *pthread_client(void *);

/* 连接上的客户端的套接字描述符 */
struct user
{
	int confd;
	char name[20];
};

struct user user_info[50];
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

int main()
{
	/* 获取套接字 */
	int sockfd, i, connfd;
	pthread_t tid;
	struct sockaddr_in server;
	struct sockaddr_in client;
	socklen_t len = sizeof(struct sockaddr);
	int optval;

	signal(SIGPIPE, SIG_IGN);

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		perror("socket");
		return -1;
	}

	/* 设置端口号支持重复绑定 */
	if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
	{
		perror("setsockopt");
		return -1;
	}

	/* 绑定自己的IP和port */
	server.sin_family = AF_INET;
	server.sin_port = htons(8192);
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	// inet_pton(AF_INET, "192.168.6.100", (void *)&server.sin_addr);
	if (bind(sockfd, (struct sockaddr *)&server, sizeof(server)) < 0)
	{
		perror("bind");
		close(sockfd);
		return -1;
	}

	listen(sockfd, 10);
	printf("wait for...\n");

	while (1)
	{
		if ((connfd = accept(sockfd, (struct sockaddr *)&client, &len)) < 0)
		{
			perror("accept");
			continue;
		}
		pthread_mutex_lock(&lock);
		for (i = 0; i < 50; i++)
		{
			if (user_info[i].confd == 0)
			{
				user_info[i].confd = connfd;
				break;
			}
		}
		pthread_mutex_unlock(&lock);
		if (i >= 50)
			continue;
		printf("recives from %s, new connfd=%d\n", inet_ntoa(client.sin_addr), connfd);
		pthread_create(&tid, NULL, pthread_client, (void *)&connfd);
	}
	close(sockfd);

	return 0;
}

void *pthread_client(void *arg)
{
	int i, connfd;
	char buf[256];
	char str[256];

	connfd = *((int *)arg);

	while (1)
	{
		/* 读取客户端内容 */
		if (read(connfd, buf, 256) <= 0)
		{
			close(connfd);
			pthread_mutex_lock(&lock);
			for (i = 0; i < 50; i++)
			{
				if (connfd == user_info[i].confd)
				{
					user_info[i].confd = 0;
					strcpy(buf, user_info[i].name);
					strcat(buf, "已经下线了!");
					break;
				}
			}
			for (i = 0; i < 50; i++)
			{
				if (user_info[i].confd != 0)
				{
					write(user_info[i].confd, buf, strlen(buf) + 1);
				}
			}
			pthread_mutex_unlock(&lock);
			pthread_exit(NULL);
		}
		else
		{
			if (buf[0] == 'g')
			{
				pthread_mutex_lock(&lock);
				for (i = 0; i < 50; i++)
				{
					if (user_info[i].confd == connfd)
					{
						sprintf(str, "%s说:%s", user_info[i].name, &buf[2]);
						break;
					}
				}
				for (i = 0; i < 50; i++)
				{
					if (user_info[i].confd != 0)
						write(user_info[i].confd, str, strlen(str) + 1);
				}
				pthread_mutex_unlock(&lock);
			}
			else if (buf[0] == 'u')
			{
				pthread_mutex_lock(&lock);
				for (i = 0; i < 50; i++)
				{
					if (user_info[i].confd == connfd)
					{
						strcpy(user_info[i].name, &buf[2]);
						break;
					}
				}
				strcat(&buf[2], "已经上线了!");
				for (i = 0; i < 50; i++)
				{
					if ((user_info[i].confd != 0) && (user_info[i].confd != connfd))
					{
						write(user_info[i].confd, &buf[2], strlen(buf) - 1);
					}
				}
				pthread_mutex_unlock(&lock);
			}
			else if (buf[0] == '@')
			{
				char *p1;
				char *p2;
				p1 = &buf[1];
				p2 = strchr(buf, ':');
				*p2 = '\0';
				p2++;
				pthread_mutex_lock(&lock);
				for (i = 0; i < 50; i++)
				{
					if (user_info[i].confd == connfd)
					{
						sprintf(str, "\033[0;34m%s悄悄对你说:%s\033[0m", user_info[i].name, p2);
						break;
					}
				}
				for (i = 0; i < 50; i++)
				{
					if (!strcmp(user_info[i].name, p1))
					{
						write(user_info[i].confd, str, strlen(str) + 1);
						break;
					}
				}
				pthread_mutex_unlock(&lock);
			}
		}
	}

	pthread_exit(NULL);
}

聊天室示例代码——客户端

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

#define CHAT_DEBUG	0
#if CHAT_DEBUG
#define DBG(x...)	printf(x)
#else
#define DBG(x...)
#endif

void* pthread_read(void *);

int main(int argc, char *argv[])
{
	/* 获取套接字 */
	int sockfd, ret;
	char buf[256];
	char str[256];
	pthread_t tid;
	struct sockaddr_in server;
	
	if (2 != argc) {
		printf("Usage:%s <ip>\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0) {
		perror("socket");
		return -1;
	}
	
	/* 连接远程服务器 */
	server.sin_family = AF_INET;
	server.sin_port = htons(8192);
	inet_pton(AF_INET, argv[1], (void *)&server.sin_addr);
	ret = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
	if(-1 == ret) {
		perror("connect");
		return -1;
	}

	pthread_create(&tid, NULL, pthread_read, (void *)&sockfd);
	
	printf("请输入登录名称:");
	scanf("%s", buf);
	sprintf(str, "u:%s", buf);
	write(sockfd, str, strlen(str)+1);
	getchar();
	
	while(1)
	{
		/* 获取终端输入 */
		printf("请输入:\n");
		fgets(buf, 256, stdin);
		buf[strlen(buf)-1] = '\0';
		DBG("b:%s#\n", buf);
		/* 发送给服务器处理 */
		if(buf[0] != '@')
			sprintf(str, "g:%s", buf);
		else {
			strcpy(str, buf);
		}
		DBG("a:%s#\n", str);
		write(sockfd, str, strlen(str)+1);
	}
	close(sockfd);
	return 0;
}

void*	pthread_read(void *arg)
{
	int sockfd = *((int *)arg);
	char str[256];
	while(1)
	{
		/* 接收服务器的数据 */
		if(read(sockfd, str, 256) <= 0)
			exit(0);
		else
			printf("%s\n:", str);
	}
}

实现高性能业务服务器

select/epoll简介

​ 通过上一个阶段的学习,我们了解和掌握了多线程、网络编程的概念和编码,也实现了一个网络聊天室的基本群聊和私聊的功能。但是有一个问题是,如果连接到服务端的客户数量过多时,由于每连接一个客户端,都需要为这个客户端单独建立一个线程来完成通信,当客户端过多时,线程就会很多。线程过多的坏处就凸显了,如下:

  • Linux每创建一个线程,系统会增加8MByte的空间用于维护线程栈、数据结构等,造成资源浪费,想象一下5000个客户端将占用40G的内存空间是多么的奢侈;

  • 线程是被系统分时调用的,过多的线程会导致过多的任务调度,浪费CPU真实执行有效代码的时
    间;

  • 过多的线程可能会有资源共享导致的互斥访问问题,导致代码互斥锁导致的效率和维护难的问题;

  • 过多的线程调试也不是一件简单的事情;

​ 由于以上原因,我们在开发一款高效的网络服务器时,应该尽量减少对线程的依赖,最好使用一个线程来完成网络的数据收发工作即可,目前我们有两个选择:select/epoll两种多路I/O复用技术。

​ 什么是I/O多路复用技术?按字面理解,是"I/O"表示输入输出,多路表示多个输入输出,复用代表重复使用,即多个输入输出可以重复在一个地方去监听使用。这是什么意思呢?我们想下和客户端通信时,read/recv读取客户端的数据是阻塞的,导致没办法单线程完成。假设现在有一个"代理",可以帮助我们监听所有的read/recv客户端是否有数据到来,那就只有这个"代理"一个阻塞就可以了,当任意客户端有数据可读时,由代理统一通知我们,这样就解决了之前必须多线程才能做到的事情,这就是I/O多路复用技术的优势,可以完成在使用很少的资源的情况下,完成很高的网络并发和吞吐能力,著名的nodejs、nginx就是依赖这种技术完成的高并发。

image-20240410174715411

​ select/epoll是两种不同实现的多路I/O复用技术。

​ select是一种轮询的机制来完成I/O端口的复用的,即select函数会把复用的所有文件描述符都不定期的去"询问"对应的描述符是否准备就绪,当文件描述符过多时,显然会影响执行的效率,并且select在内核中的实现是通过把读到的数据拷贝到用户空间的,效率上也会比较低,并且最大的问题是select的最大支持文件描述符个数是1024个,也导致了不能做为高性能服务器的主要技术。

​ epoll不同于select的实现。我们知道,最高效的方式应该是当客户端有数据过来时,程序再执行,没有数据过来时,程序应该睡眠。epoll就是按照这种思路实现的,并且当读到数据后,是直接把数据映射到用户空间,也少了一次数据的拷贝,非常高效。另外epoll由于不是轮询的机制实现,在支持的文件描述符上也不会有1024的限制。而且还有一点更加高效的是epoll支持边沿触发,即当在文件描述符上发生了事件时,只会通知一次,这就要求我们在代码的编写时,要非常小心的做一些额外的错误处理,否则会丢失要读写的数据。

​ 接下来就让我们一起来看一下,如何通过边沿触发的方式来实现一个高效的服务端的核心代码部分。以下分为两个部分,一个是函数说明,一个是代码示例。

epoll相关函数说明

epoll_create

/**
* @brief 创建一个epoll多路IO复用对象
*
* @param[int] size 是非负数,表示能够监听的最大文件描述符个数
*
* @return 成功返回非负文件描述符,错误返回-1,错误码存放在errno中
*
*/
int epoll_create(int size);

epoll_ctl

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event {
    uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};
/**
* @brief 把fd的event事件的操作op放到epoll中
*
* @param[in] epfd 表示epoll的文件描述符
* @param[in] op 操作方式,有如下三种
* 三大功能:
* 	EPOLL_CTL_ADD:增加一个文件描述符fd的事件event到epfd中
* 	EPOLL_CTL_MOD:修改一个文件描述符fd所关心的事件event到epfd中
* 	EPOLL_CTL_DEL:删除一个文件描述符fd的事件event
*
* @param[in] fd 被操作的文件描述符
* @param[in] event 被操作的文件描述符的事件,如上所示的结果体,其中的events包含以下几类:
* 	EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
* 	EPOLLOUT 表示对应的文件描述符可以写;
* 	EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
* 	EPOLLERR 表示对应的文件描述符发生错误;
* 	EPOLLHUP 表示对应的文件描述符被挂断;
* 	EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式,默认的是水平触发(LevelTriggered)。
* 	EPOLLONESHOT 只监听一次事件,监听完之后,如果还需要继续监听,需要再次把事件加入到EPOLL队列里。
* @return 成功返回0,调用失败返回错误码
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_wait

/**
* @brief 等待一个I/O事件的发生
*
* @param[in] epfd 用于等待事件发生的epoll文件描述符
* @param[in] events 发生事件的文件描述符集
* @param[in] maxevents 一次最大返回的事件个数
* @param[in] timeout 等待的超时事件,以毫秒为单位。0会立即返回,-1会一直阻塞等待事件发生,大于0为超时时间
* @return 成功返回发生的文件描述符时间的个数,超时返回0,错误返回-1,错误码存放于errno中
*
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

fcntl

函数原型:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// arg表示可变参数,由cmd决定

fcntl()对打开的文件描述符fd执行下面描述的操作之一。操作由cmd决定。

fcntl()的第三个参数是可选。是否需要此参数由cmd决定。所需的参数类型在每个cmd名称后面的括号中指示(在大多数情况下,所需的类型是int,我们使用名称arg来标识参数),如果不需要参数,则指定void。

以下某些操作仅在特定的Linux内核版本之后才受支持。检查主机内核是否支持特定操作的首选方法是使用所需的cmd值调用fcntl(),然后使用EINVAL测试调用是否失败,这表明内核无法识别该值。

主要介绍下面4个功能:

​ 1、复制文件描述符(F_DUPFD、F_DUPFD_CLOEXEC);
​ 2、获取/设置文件描述符标志(F_GETFD、F_SETFD);
​ 3、获取/设置文件状态标志(F_GETFL、F_SETFL);
​ 4、获取/设置记录锁(F_GETLK、F_SETLK、F_SETLKW);

epoll高效服务器示例代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define MAX_BUFF        4096

static int set_nonblock(int fd)
{
    int opts;
    opts = fcntl(fd, F_GETFL);
    if (opts < 0) {
        perror("fcntl F_GETFL");
        return -1;
    }
    opts = opts | O_NONBLOCK;
    if (fcntl(fd, F_SETFL, opts) < 0) {
        perror("fcntl F_SETFL O_NONBLOCK");
        return -1;
    }
    return 0;
}

static int set_recvbuff(int fd, int nBuffLen)
{
    return setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &nBuffLen, sizeof(nBuffLen));
}

static int get_recvbuff(int fd)
{
    int nValue = -1;
    socklen_t nValueLen = sizeof(int);
    if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &nValue, &nValueLen)) {
        perror("getsockopt");
    }
    return nValue;
}

static int socket_init(short nPort)
{
    struct sockaddr_in stServerAddr;
    /* 创建一个socket对象 */
    int nListenFd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == nListenFd) {
        perror("socket");
        return -1;
    }

    /* 使得绑定端口号可以复用,解决重新运行时的address is already in use的错误 */
    socklen_t nReuseAddr = 1;
    if (setsockopt(nListenFd, SOL_SOCKET, SO_REUSEADDR, (const void *)&nReuseAddr, sizeof(socklen_t))) {
        perror("set reuse addr");
        close(nListenFd);
        return -1;
    }
    if (set_recvbuff(nListenFd, MAX_BUFF)) {
        perror("set_recvbuff");
        close(nListenFd);
        return -1;
    }
    bzero(&stServerAddr, sizeof(stServerAddr));
    stServerAddr.sin_family = AF_INET;
    stServerAddr.sin_port = htons(nPort);
    stServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* stServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
    if (bind(nListenFd, (struct sockaddr *)&stServerAddr, sizeof(stServerAddr))) {
        perror("bind");
        close(nListenFd);
        return -1;
    }
    listen(nListenFd, 20);
    return nListenFd;
}

static int epoll_init(int fd)
{
    struct epoll_event ev;
    /* 把socket设置为非阻塞方式 */
    set_nonblock(fd);
    /* 创建epoll多路复用接口 */
    int nEpollFd = epoll_create(1024);
    if (-1 == nEpollFd) {
        perror("epoll_create");
        return -1;
    }
    /* 设置要处理的事件类型为边沿触发读事件,并注册到epoll中 */
    ev.data.fd = fd;
    ev.events = EPOLLIN | EPOLLET;
    if (epoll_ctl(nEpollFd, EPOLL_CTL_ADD, fd, &ev)) {
        perror("epoll_ctl");
        close(nEpollFd);
        return -1;
    }
    return nEpollFd;
}

static int epoll_run(int nListenFd, int nEpollFd)
{
    int fd;
    ssize_t n;
    socklen_t nClientAddrLen;
    char sRecvBuffer[MAX_BUFF];
    struct epoll_event ev, events[20];
    struct sockaddr_in stClientAddr;
    int nLoop = 1;
    while (nLoop)
    {
        /* 等待epoll事件的发生 */
        int nfds = epoll_wait(nEpollFd, events, 20, -1);
        /* 处理所发生的所有事件 */
        for (int i = 0; i < nfds; ++i) {
            /* 如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接 */
            if (events[i].data.fd == nListenFd) {
                int nConnFd = accept(nListenFd, (struct sockaddr *)&stClientAddr, &nClientAddrLen);
                if (nConnFd < 0) {
                    perror("accept");
                    break;
                }
                printf("accapt a connection from:%s, port:%d\n", inet_ntoa(stClientAddr.sin_addr), ntohs(stClientAddr.sin_port));
                /* 设置客户端文件描述符为非阻塞方式 */
                if (set_nonblock(nConnFd)) {
                    close(nConnFd);
                    perror("set_nonblock");
                    continue;
                }
                /* 注册nConnFd到epoll中,边沿方式监听是否有数据可读 */
                ev.data.fd = nConnFd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(nEpollFd, EPOLL_CTL_ADD, nConnFd, &ev);
            }
            /* 判断是否有数据可读 */
            else if (events[i].events & EPOLLIN)
            {
                int nCount = 0;
                if ((fd = events[i].data.fd) < 0) continue;
                while(n = read(fd, sRecvBuffer+nCount, MAX_BUFF-1-nCount)) {
                    if (n <= 0) {
                        if (EAGAIN != n) {
                            sRecvBuffer[nCount] = '\0';
                            printf("read %s\n", sRecvBuffer);
                        }
                        else {
                            ev.data.fd = fd;
                            epoll_ctl(nEpollFd, EPOLL_CTL_DEL, fd, NULL);
                            close(fd);
                            printf("fd:%d is closed\n", fd);
                        }
                        break;
                    }
                    nCount += n;
                }
                /* 修改为边沿方式监听是否有数据可写 */
                ev.data.fd = fd;
                ev.events = EPOLLOUT | EPOLLET;
                epoll_ctl(nEpollFd, EPOLL_CTL_MOD, fd, &ev);
            }
            /* 判断是否有数据可写 */
            else if (events[i].events & EPOLLOUT) {
                fd = events[i].data.fd;
                write(fd, sRecvBuffer, n);
                /* 修改为边沿方式监听是否有数据可读 */
                ev.data.fd = fd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(nEpollFd, EPOLL_CTL_MOD, fd, &ev);
            }
        }
    }
}

int main(int argc, char *argv[])
{
    int nListenFd = -1, nEpollFd = -1;
    if (2 != argc) {
        fprintf(stderr, "Usage:%s port\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    do {
        nListenFd = socket_init(atoi(argv[1]));
        if (-1 == nListenFd) break;
        nEpollFd = epoll_init(nListenFd);
        if (-1 == nEpollFd) break;

        epoll_run(nListenFd, nEpollFd);
    } while(0);
    
    if (nEpollFd > 0) close(nEpollFd);
    if (nListenFd > 0) close(nListenFd);
    return 0;
}

posted @ 2024-04-30 15:31  爱吃冰激凌的黄某某  阅读(1)  评论(0编辑  收藏  举报