网络
网络基础
怎么解决通信设备间的通信问题?解决这个问题,各设备就必须要使用同一套通信协议,才能互相理解对方“说的话”,目前在互联网中这个一直被我们使用的协议叫TCP/IP协议簇,简称TCP/IP。其中TCP是Transmission Control Protocol的简称,它是一种面向连接的、可靠的、基于字节流的传输层通信协议,IP是Internet Protocol的简称,它的任务仅仅是根据源主机和目的主机的地址来传送数据。
TCP/IP协议分为4层
IP地址网段和分类
4层架构在RFC 1122中描述的不同层数据的封装
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编程流程
基于数据报文的编程流程
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)]; };
举例:
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
- 端口号为SERV_PORT, 我们定义为8080;
socket
功能: 创建socket套接字
bind
listen
accept
connect
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
:为接受数据的系统调用设置超时,包括:recv
,recvfrom
,accept
;若超时, 返回-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就是依赖这种技术完成的高并发。
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;
}
标签:addr,epoll,int,31,网络,fd,include,struct
From: https://www.cnblogs.com/mzx233/p/18168117