socket编程
socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/*
功能:创建一个套接字,返回对应的文件描述符
domin: 协议族
指定套接字使用的通信协议族,常见选项:
AF_INET:IPv4 协议(最常用)。
AF_INET6:IPv6 协议。
AF_UNIX 或 AF_LOCAL:本地进程间通信(Unix 域套接字)。
AF_PACKET:底层数据包接口(如原始套接字)。
type:套接字的类型
SOCK_STREAM:面向连接的流套接字(如 TCP,可靠、有序、双向)。
SOCK_DGRAM:无连接的数据报套接字(如 UDP,不可靠、有大小限制)。
SOCK_RAW:原始套接字(直接访问网络层,需管理员权限)
protocol:具体协议
通常设置为 0,表示根据 domain 和 type 自动选择默认协议。例如:
IPPROTO_TCP(TCP 协议)。
IPPROTO_UDP(UDP 协议)。
原始套接字中可指定 IPPROTO_ICMP 等
返回值:成功返回文件描述符,失败返回-1并设置错误码
*/
bind
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
为套接字绑定端口和ip
sockfd:套接字的文件描述符
addr:见下
addrlen: addr结构的大小(字节)
成功返回0,失败返回-1并设置错误码
*/
struct sockaddr* addr
{
sa_family_t sin_family; // 地址族,如 AF_INET(IPv4)
in_port_t sin_port; // 端口号(16位,需用 htons() 转换字节序)
struct in_addr sin_addr; // IP 地址(32位)
char sin_zero[8]; // 填充字段(通常置 0)
}
struct in_addr {
uint32_t s_addr; // 32位 IPv4 地址(需用 inet_addr() 或 INADDR_ANY)
};
listen
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd,int backlog);
/*
将套接字设置为被动监听状态
sockfd:已经被bind绑定过的套接字文件描述符
backlog:等待连接队列的最大长度
成功返回0,失败返回-1并设置错误码
*/
accept
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
等待客户端的连接请求,返回一个新的与客户端通信用到专用套接字conn_fd
addr:将要接受的客户端套接字地址信息
成功返回conn_fd,失败返回-1并设置错误码
*/
recv/send
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*
从套接字中读取信息并放入buf中
参数 说明
sockfd 已连接的套接字描述符(由 accept() 返回)
buf 接收数据的缓冲区
len 缓冲区长度
flags 控制接收行为的标志(常见值见下表)
标志 说明
0 默认行为(阻塞读取)
MSG_WAITALL 等待直到缓冲区填满(除非发生错误或连接关闭)
MSG_DONTWAIT 非阻塞读取(立即返回,无数据时返回 EAGAIN)
MSG_PEEK 查看数据但不从接收队列移除
成功:返回实际接收的字节数(>0)。
连接关闭:返回 0(对方调用了 close())。
失败:返回 -1,并设置 errno
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*
将buf中消息写入套接字
成功:返回实际发送的字节数(可能小于 len)。
失败:返回 -1,并设置 errno
*/
close
#include <unistd.h>
int close(int fd);
//关闭套接字
//成功返回0,失败返回-1并设置错误码
connect
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
向服务端发起连接请求
sockfd:要连接的套接字
addr:指向服务器的套接字地址
成功返回0,失败返回-1并设置errno
*/
recvfrom/sendto
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/*
从套接字sockfd中读取len长的消息放入buf中,并接受对端地址信息结构体
flags:是否阻塞,0表示阻塞 , MSG_DONTWAIT表示非阻塞
src_addr:对端地址信息
成功返回读取字节的个数,失败返回-1并设置错误码
*/
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
/*
向套接字sockfd中发送消息,并指定对方地址信息结构体
成功返回发送字节的个数,失败返回-1并设置错误码
*/
代码实现
TCP服务器实现(linus)
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
int main(int argc,const char* argv[])
{
//创建套接字
int fd;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket error");
return -1;
}
printf("socket success fd = %d\n", fd);
//创建套接字地址
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
sin.sin_port = htons(SERVICE_PORT);
if (bind(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//启动监听
if (listen(fd, 128) == -1)
{
perror("listen error\n");
return -1;
}
printf("listen success\n");
//阻塞等待客户端的连接请求
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
int newfd = accept(fd, (struct sockaddr*)&cin, &socklen);
if (newfd == -1)
{
perror("accept error\n");
return -1;
}
printf("[%s:%d]connect success\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
char rbuf[128] = "";
while (1)
{
memset(rbuf, 0,sizeof(rbuf));
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if (res == 0)
{ // 客户端关闭连接
printf("传输结束\n");
break;
}
else if (res == -1)
{ // 出错
perror("recv error");
break;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
strcat(rbuf, "已接收");
if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
printf("发送成功\n");
}
close(newfd);
close(fd);
printf("end....");
return 0;
}
TCP客户端实现
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
#define CLIENT_IP "192.168.174.128"
#define CLIENT_PORT 9999
int main(int argc,const char* argv[])
{
int cfd = socket(AF_INET,SOCK_STREAM,0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n",cfd);
// struct sockaddr_in cin;
// cin.sin_family = AF_INET;
// cin.sin_port = htons(CLIENT_PORT);
// cin.sin_addr.s_addr = inet_addr(CLIENT_IP);
// if(bind(cfd,(struct sockaddr*)&cin,sizeof(cin)) == -1)
// {
// perror("bind error");
// return -1;
// }
// printf("bind success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(connect(cfd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("connect error\n");
return -1;
}
printf("connect success\n");
char wbuf[128] = "";
while(1)
{
memset(wbuf,0,sizeof(wbuf));
fgets(wbuf,sizeof(wbuf),stdin);
wbuf[strlen(wbuf)-1] = 0;
if(send(cfd,wbuf,sizeof(wbuf),0) == -1)
{
perror("send error");
return -1;
}
if(recv(cfd,wbuf,sizeof(wbuf),0) == 0)
{
printf("服务器关闭\n");
return -1;
}
printf("receive :%s\n",wbuf);
}
close(cfd);
printf("end...\n");
return 0;
}
UDP服务器
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
char rbuf[128] = "";
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
memset(rbuf,0,sizeof(rbuf));
int rc;
if((rc = recvfrom(fd,rbuf,sizeof(rbuf),0,(struct sockaddr*)&cin,&socklen)) == -1)
{
perror("recv error");
return -1;
}
else if(rc == 0)
{
printf("连接断开..\n");
break;
}
printf("[%s:%d] %s\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),rbuf);
strcat(rbuf,"已接受");
if(sendto(fd,rbuf,sizeof(rbuf),0,(struct sockaddr*)&cin,sizeof(cin)) == -1)
{
perror("send error");
return -1;
}
}
close(fd);
return 0;
}
UDP客户端
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
#define CLIENT_IP "192.168.174.128"
#define CLIENT_PORT 9999
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_DGRAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
// struct sockaddr_in sin;
// sin.sin_family = AF_INET;
// sin.sin_port = htons(SERVICE_PORT);
// sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
// if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
// {
// perror("bind error");
// return -1;
// }
// printf("bind success\n");
char wbuf[128] = "";
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
sin.sin_port = htons(SERVICE_PORT);
while(1)
{
memset(wbuf,0,sizeof(wbuf));
fgets(wbuf,sizeof(wbuf),stdin);
wbuf[strlen(wbuf)-1] = 0;
int sd;
if((sd = sendto(fd,wbuf,sizeof(wbuf),0,(struct sockaddr*)&sin,sizeof(sin))) == -1)
{
perror("send error");
return -1;
}
else if(sd == 0)
{
printf("连接断开..\n");
break;
}
if(recvfrom(fd,wbuf,sizeof(wbuf),0,NULL,NULL) == -1)
{
perror("send error");
return -1;
}
printf("%s\n",wbuf);
}
close(fd);
return 0;
}
并发服务器
accept函数和recv函数都是阻塞函数,会干扰相互间的运行,可以用并发机制
多进程
主进程完成客户端的连接请求,子进程完成对客户端的通信操作
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<signal.h>
#include<sys/wait.h>
#include<stdlib.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
void handler(int signal)
{
if(signal == SIGCHLD)
while(waitpid(-1,NULL,WNOHANG)>0);
}
int main(int argc,const char* argv[])
{
if(signal(SIGCHLD,handler) == SIG_ERR)
{
perror("signal error");
return -1;
}
printf("signal success\n");
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
if(listen(fd,128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
int newfd = accept(fd,(struct sockaddr*)&cin,&socklen);
if(newfd == -1)
{
perror("accept error\n");
return -1;
}
printf("[%s:%d] fd = %d connect!\n", inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),newfd);
pid_t pid = fork();
if(pid > 0)
{
close(newfd);
}
else if(pid == 0)
{
close(fd);
char rbuf[128] = "";
while(1)
{
memset(rbuf,0,sizeof(rbuf));
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if (res == 0)
{
printf("传输结束\n");
break;
}
else if (res == -1)
{
perror("recv error");
break;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
strcat(rbuf, "已接收");
if (send(newfd, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
}
close(newfd);
exit(0);
}
else{
perror("fork error");
return -1;
}
}
close(fd);
return 0;
}
多线程
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
struct INFO
{
int newfd;
struct sockaddr_in addr;
};
void* cli_msg(void* buf)
{
int newfd = ((struct INFO*)buf)->newfd;
struct sockaddr_in cin = ((struct INFO*)buf)->addr;
char rbuf[128] = "";
while(1)
{
memset(rbuf,0,sizeof(rbuf));
int rc = recv(newfd,rbuf,sizeof(rbuf),0);
if(rc == -1)
{
perror("recv error");
return NULL;
}
else if(rc == 0)
{
printf("该客户端关闭\n");
break;
}
printf("[%s:%d]:%s\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),rbuf);
strcat(rbuf,"已接收");
if(send(newfd,rbuf,sizeof(rbuf),0) == -1)
{
perror("send error");
return NULL;
}
}
close(newfd);
pthread_exit(NULL);
}
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
if(listen(fd,128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1)
{
int newfd = accept(fd,(struct sockaddr*)&cin,&socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
struct INFO buf = {newfd,cin};
pthread_t pid = 1;
if(pthread_create(&pid,NULL,cli_msg,&buf) != 0)
{
perror("pthread error");
return -1;
}
pthread_detach(pid);
}
close(fd);
return 0;
}
IO多路复用
select
监控多个文件描述符(如套接字、管道等)的状态变化(可读、可写或异常)。它是实现高性能网络服务器的传统方法之一,尽管现代系统更推荐使用 epoll
或 kqueue
。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
struct timespec
{
long tv_sec;
long tv_nsec; // 纳秒
}
参数说明
参数 | 类型 | 说明 |
---|---|---|
nfds | int | 监控的文件描述符最大值 + 1(如监控 fd 3 和 fd 5 ,则 nfds=6 )。 |
readfds | fd_set * | 监控 可读事件 的文件描述符集合(传入需监控的fd,返回就绪的fd)。 |
writefds | fd_set * | 监控 可写事件 的文件描述符集合(用法同 readfds )。 |
exceptfds | fd_set * | 监控 异常事件 的文件描述符集合(如带外数据)。 |
timeout | struct timeval * | 超时时间:NULL (阻塞)、0 (非阻塞)、>0 (定时阻塞)。 |
返回值
- > 0:就绪的文件描述符总数。
- = 0:超时且无就绪事件。
- -1:出错(如被信号中断,需检查
errno
)。
关键操作宏
fd_set
是一个文件描述符集合,通过以下宏操作:
宏 | 功能 |
---|---|
FD_ZERO(fd_set *) | 清空集合。 |
FD_SET(int fd, fd_set *) | 将 fd 加入集合。 |
FD_CLR(int fd, fd_set *) | 将 fd 从集合移除。 |
FD_ISSET(int fd, fd_set *) | 检查 fd 是否在集合中(就绪时返回真)。 |
select实现服务器
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
if(listen(fd,128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
fd_set readfds,tempfds;
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(fd,&readfds);
int maxfd = fd;
int newfd = -1;
struct sockaddr_in cin_arr[128];
while(1)
{
tempfds = readfds;
int res = select(maxfd+1,&tempfds,NULL,NULL,NULL);
if(res == -1)
{
perror("select error");
return -1;
}
else if(res == 0)
{
printf("time out!!\n");
return -1;
}
if(FD_ISSET(fd,&tempfds))
{
newfd = accept(fd,(struct sockaddr*)&cin,&socklen);
if(newfd == -1)
{
perror("accpet error");
return -1;
}
printf("[%s:%d]:connect success newfd = %d\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),newfd);
cin_arr[newfd] = cin;
FD_SET(newfd,&readfds);
maxfd = (maxfd>newfd?maxfd:newfd);
}
if(FD_ISSET(0,&tempfds))
{
for(int i = 4;i<=maxfd;i++)
{
if(FD_ISSET(i,&tempfds))
{
char rbuf[128] = "";
memset(rbuf,0,sizeof(rbuf));
int res = recv(i,rbuf,sizeof(rbuf),0);
if(res == 0)
{
printf("客户端关闭\n");
close(i);
FD_CLR(i,&readfds);
for(int k = maxfd;k>=0;k--)
{
if(FD_ISSET(k,&readfds))
{
maxfd = k;
break;
}
}
continue;
}
else if(res == -1){
perror("recv error");
return -1;
}
printf("[%s:%d]:%s\n",inet_ntoa(cin_arr[i].sin_addr),ntohs(cin_arr[i].sin_port),rbuf);
strcat(rbuf,"已接收");
if(send(i,rbuf,sizeof(rbuf),0) == -1)
{
perror("send error");
return -1;
}
}
}
}
}
close(fd);
return 0;
}
poll
略。。
epoll
epoll全称为eventpoll,是内核实现IO多路复用的一种实现方式。在其中一个或多个事件的到满足时,可以解除阻塞。epoll是select和poll的升级版,相比于select和poll而言,epoll改进了工作方式,效率更高。
与select、poll的区别
1. 核心区别总览
特性 select
poll
epoll
时间复杂度 O(n) - 线性扫描所有 fd O(n) - 线性扫描所有 fd O(1) - 仅通知就绪的 fd 监控数量限制 受 FD_SETSIZE
限制(1024)无硬性限制 仅受系统内存限制 触发模式 水平触发(LT) 水平触发(LT) 支持 LT 和 ET(边缘触发) 内核交互方式 每次调用需传递完整 fd 集合 同 select
内核维护事件表,无需重复传递 跨平台性 广泛支持(POSIX) 多数 Unix 系统 Linux 特有 适用场景 少量 fd、跨平台 需更多 fd 的简单场景 高并发(C10K+)
2. 设计原理对比
(1)
select
- 实现方式:
通过位图(fd_set
)管理 fd,每次调用需将完整 fd 集合从用户态拷贝到内核态,内核线性扫描所有 fd。- 缺点:
- 每次调用需重置 fd 集合。
- 监控 fd 数量受限(默认 1024)。
- 频繁拷贝和扫描导致性能瓶颈。
(2)
poll
- 改进点:
用动态数组(struct pollfd
)替代fd_set
,突破 fd 数量限制。- 剩余问题:
仍需线性扫描和重复传递 fd 集合,性能与select
相近。(3)
epoll
- 创新设计:
- 红黑树 + 就绪链表:内核维护一个红黑树存储所有监控的 fd,就绪的 fd 被加入链表,无需重复扫描。
- 事件回调机制:仅返回就绪的 fd 列表,时间复杂度 O(1)。
- 优势:
- 支持边缘触发(ET),减少无效事件通知。
- 无 fd 数量限制,适合高并发。
3. 性能对比
10,000 个非活跃连接下的性能
指标 select
/poll
epoll
CPU 占用 高(每次扫描所有 fd) 极低(仅处理就绪 fd) 内存占用 低(固定大小数据结构) 较高(需维护内核事件表) 延迟 随 fd 数量增加而升高 稳定 O(1)
4. 触发模式详解
(1) 水平触发(LT)
行为:
只要 fd 处于就绪状态(如缓冲区有数据),每次调用都会通知。特点:
- 类似
select
/poll
的行为。- 若不处理完数据,会持续触发。
代码示例:
// epoll LT 模式(默认) ev.events = EPOLLIN; // 水平触发读事件
(2) 边缘触发(ET)
行为:
仅在 fd 状态变化时通知一次(如从不可读到可读)。要求:
- 必须使用非阻塞 I/O。
- 需循环读写直到返回
EAGAIN
。代码示例:
// epoll ET 模式 ev.events = EPOLLIN | EPOLLET; // 边缘触发读事件 fcntl(fd, F_SETFL, O_NONBLOCK); // 设为非阻塞
- sellect和poll都是基于线性结构进行检测集合,而epoll是基于树形结构(红黑树)完成管理检测集合的
- select和poll检测时,随着集合的增大,效率会越来越低。epoll使用的是函数回调机制,效率较高。 处理文件描述符的效率也不会随着文件秒数的增大而降低。
- select和poll在工作过程中,不断的在内核空间与用户空间频繁拷贝文件描述符的数据。epoll在注册 新的文件描述符或者修改文件描述符时,只需进行一次,能够有效减少数据在用户空间和内核空间之间的切换
- 和poll一样,epoll没有最大文件描述符的限制,仅仅收到程序能够打开的最大文件描述符数量限制
- 对于select和poll而言,需要对返回的文件描述符集合进行判断后才知道时哪些文件描述符就绪了,而 epoll可以直接得到已经就绪的文件描述符,无需再次检测
- 当多路复用比较频发进行、IO流量频繁的时候,一般不使用select和poll,使用epoll比较合适
- epoll只适用于linux平台,不能跨平台操作
API
#include <sys/epoll.h>
int epoll_create(int size);
/*
功能:创建一个epoll实例,并返回该实例的句柄,是一个文件描述符
参数1:epoll实例中能够容纳的最大节点个数,自从linux 2.6.8版本后,size可以忽略,但是必须要是一个大于0的数字
返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
功能:完成对epoll实例的控制
参数1:通过epoll_create创建的epoll实例文件描述符
参数2:op表示要进行的操作
EPOLL_CTL_ADD:向epoll树上添加新的要检测的文件描述符
EPOLL_CTL_MOD:改变epoll树上的文件描述符检测的事件
EPOLL_CTL_DEL:删除epoll树上的要检测的文件描述符,此时参数3可以省略填NULL
参数3:要检测的文件描述符
参数4:要检测的事件,是一个结构体变量地址,属于输入变量
成功返回0,失败返回-1并设置错误码
*/
typedef union epoll_data
{
void *ptr; //提供的解释性数据
int fd; //文件描述符(常用)
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /*
要检测的事件:
EPOLLIN:读事件
EPOLLOUT:写事件
EPOLLERR:异常事件
EPOLLET:表示设置epoll的模式为边沿触发模式
*/
epoll_data_t data; /* 用户有效数据,是一个共用体 */
};
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
功能:阻塞检测epoll实例中是否有文件描述符准备就绪,如果准备就绪了,就解除阻塞
参数1:epoll实例对于的文件描述符
参数2:文件描述符集合,当有文件描述符产生事件时,将所有产生事件的文件描述符,放入到该集合中
参数3:参数2的大小
参数4:超时时间,以毫秒为单位的超时时间,如果填-1表示永久阻塞
返回值:
>0:表示解除本次操作的触发的文件描述符个数
=0:表示超时,但是没有文件描述符产生事件
=-1:失败,置位错误码
*/
epoll实现服务器
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#define SERVICE_IP "192.168.174.128"
#define SERVICE_PORT 8888
int main(int argc,const char* argv[])
{
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVICE_PORT);
sin.sin_addr.s_addr = inet_addr(SERVICE_IP);
if(bind(fd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
if(listen(fd,128) == -1)
{
perror("listen error");
return -1;
}
printf("listen success\n");
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
int epfd = epoll_create(1);
if(epfd == -1)
{
perror("epoll_create error");
return -1;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
struct epoll_event evs[128];
int size = sizeof(evs)/sizeof(evs[0]);
while(1)
{
int num = epoll_wait(epfd,evs,size,-1);
for(int i = 0;i<num;i++)
{
int FD = evs[i].data.fd;
if(FD == fd)
{
int newfd = accept(fd,(struct sockaddr*)&cin,&socklen);
if(newfd == -1)
{
perror("accept error");
return -1;
}
printf("[%s:%d]:connect success newfd = %d\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),newfd);
struct epoll_event nev;
nev.events = EPOLLIN;
nev.data.fd = newfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&nev);
}
else
{
char rbuf[128] = {};
memset(rbuf,0,sizeof(rbuf));
int res = recv(FD, rbuf, sizeof(rbuf), 0);
if (res == 0)
{
printf("传输结束\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,FD,NULL);
close(FD);
break;
}
else if (res == -1)
{
perror("recv error");
break;
}
printf("%s\n", rbuf);
strcat(rbuf, "已接收");
if (send(FD, rbuf, strlen(rbuf), 0) == -1)
{
perror("send error");
return -1;
}
}
}
}
close(fd);
close(epfd);
return 0;
}
水平模式与边沿模式
- 事件触发告知条件
- 可读事件
- 内核网络读缓存区只要有数据就会触发读事件
- 只要全连接队列有数据,有连接没有处理,就触发读事件
- 对端写端关闭,触发当前端读事件
- 可写事件
- 内核网络写缓存区只要有可写空间就触发可写事件
- connect连接建立成功触发可写事件
- 可读事件
- 区别
- 水平触发只要满足io就绪条件就触发
- 如果没有处理事件,下次epoll_wait仍可以获得该事件
- 如果没有处理完全,也可以获得该事件
- 边缘触发只有新的io就绪事件到来才触发
- 如果没有处理,下一次epoll_wait获得该未处理事件
- 如果没处理完全,同样
- 水平触发只要满足io就绪条件就触发
组播和广播
广播 vs 组播对比
特性 | 广播(Broadcast) | 组播(Multicast) |
---|---|---|
地址范围 | 子网内所有主机(如 192.168.1.255 ) | 特定组播组(如 224.0.0.100 ) |
网络负载 | 高(所有主机接收) | 低(仅订阅组接收) |
跨子网支持 | 不支持(路由器阻断) | 支持(需路由器配置 IGMP) |
适用场景 | 局域网发现(如 DHCP) | 视频会议、实时数据分发 |
网络属性
#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);
/*
获取或设置套接字文件描述符的属性
参数1:套接字文件描述符
参数2:表示操作的套接字层
SOL_SOCKET:表示应用层或者套接字层,通过man 7 socket进行查找 该层中常用的属性:
SO_REUSEADDR 地址快速重用
SO_BROADCAST 允许广播
SO_RCVTIMEO and SO_SNDTIMEO:发送或接收超时时间
IPPROTO_TCP:表示传输层的基于TCP的协议
IPPROTO_UDP:表示传输层中基于UDP的协议
IPPROTO_IP:表示网络层 该层常用的属性
IP_ADD_MEMBERSHIP,加入多播组
参数3:对应层中属性的名称
参数4:参数3属性的值,一般为int类型,其他类型,会给出
参数5:参数4的大小
返回值:成功返回0,失败返回-1并置位错误码
*/
广播
1>主机之间是一对多的通信方式,网络对其中的每一台主机都会将消息进行转发
2> 在当前网络下的所有主机都会收到该消息,无论是否愿意收到消息
3> 基于无连接的通信方式完成,UDP通信
4> 广播分为发送端和接收端
5> 对于发送端而言,需要设置允许广播,并且向广播地址发送数据
6> 广播地址:网络号 + 255的主机号,可以通过指令 ifconfifig查看
7> 广播消息不能穿过路由器到骨干路由上
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main(int argc, const char *argv[]) {
int sndfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sndfd == -1) {
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n", sndfd);
int broad = 1;
if(setsockopt(sndfd, SOL_SOCKET, SO_BROADCAST, &broad, sizeof(broad)) == -1) {
perror("setsockopt error");
return -1;
}
printf("成功设置广播\n");
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888);
rin.sin_addr.s_addr = inet_addr("192.168.174.255");
char wbuf[128] = "";
while(1) {
bzero(wbuf, sizeof(wbuf));
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf)-1] = 0;
sendto(sndfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&rin, sizeof(rin));
printf("发送成功\n");
}
close(sndfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main(int argc, const char *argv[]) {
int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
if(recvfd == -1) {
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", recvfd);
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888);
rin.sin_addr.s_addr = inet_addr("192.168.174.255");
if(bind(recvfd, (struct sockaddr*)&rin, sizeof(rin)) == -1) {
perror("bind error");
return -1;
}
printf("bind success\n");
char rbuf[128] = "";
struct sockaddr_in cin;
socklen_t socklen = sizeof(cin);
while(1) {
bzero(rbuf, sizeof(rbuf));
if(recvfrom(recvfd, rbuf, sizeof(rbuf), 0, (struct sockaddr*)&cin, &socklen) == -1) {
perror("recvfrom error");
return -1;
}
printf("[%s:%d]:%s\n", inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), rbuf);
}
close(recvfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
组播
1> 广播是给同一个网络下的所有主机发送消息,会占用大量的网络带宽,影响正常网络通信,造成网络拥塞
2> 组播也是实现一对多的通信机制,也就是说,加入同一个多播组的成员都能收到组播消息
3> 组播也是使用UDP实现的,发送者发送的消息,无论接收者愿不愿意,都会收到消息
4> 组播地址:D类网络地址 (224.0.0.0 — 239.255.255.255)
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main(int argc, const char *argv[]) {
int sendfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sendfd == -1) {
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n", sendfd);
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888);
rin.sin_addr.s_addr = inet_addr("224.1.2.3");
char wbuf[128] = "";
while(1) {
bzero(wbuf, sizeof(wbuf));
fgets(wbuf, sizeof(wbuf), stdin);
wbuf[strlen(wbuf)-1] = 0;
sendto(sendfd, wbuf, strlen(wbuf), 0, (struct sockaddr*)&rin, sizeof(rin));
}
close(sendfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main(int argc, const char *argv[]) {
int recvfd = socket(AF_INET, SOCK_DGRAM, 0);
if(recvfd == -1) {
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", recvfd);
struct ip_mreqn imr;//多播组属性结构体
imr.imr_multiaddr.s_addr = inet_addr("224.1.2.3");//组播地址
imr.imr_address.s_addr = inet_addr("192.168.174.128");//本机ip地址
imr.imr_ifindex = 2;//网卡编号
if(setsockopt(recvfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, sizeof(imr)) == -1) {
perror("setsockopt error");
return -1;
}
printf("成功加入多播组\n");
struct sockaddr_in rin;
rin.sin_family = AF_INET;
rin.sin_port = htons(8888);
rin.sin_addr.s_addr = inet_addr("224.1.2.3");
if(bind(recvfd, (struct sockaddr*)&rin, sizeof(rin)) == -1) {
perror("bind error");
return -1;
}
printf("bind success\n");
char rbuf[128] = "";
while(1) {
bzero(rbuf, sizeof(rbuf));
if(recvfrom(recvfd, rbuf, sizeof(rbuf), 0, NULL, NULL) == -1) {
perror("recvfrom error");
return -1;
}
printf("[读取的消息为]:%s\n", rbuf);
}
close(recvfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
域套接字
1> 域套接字就是原始的套接字通信方式,跟IPC通信方式一样,属于同一主机之间的多个进程间通信方式
2> 由于不需要进行跨主机通信了,也就不需要使用ip地址和端口号了
3> 域套接字通信载体为套接字文件linux中文件类型:bcd-lsp 其中 s类型的文件就是套接字文件
4> 通信本质:使用内核空间完成数据的传输
5> 域套接字通信也分为流式域套接字和报式域套接字
域套接字核心概念
- 定义:一种进程间通信(IPC)机制,允许同一主机上的进程通过文件系统路径名通信(不经过网络协议栈)。
- 特点:
- 高性能:比TCP/IP套接字更快(无需协议处理)。
- 仅限本地:不能跨主机通信。
- 文件系统可见:通信端点表现为文件(需注意权限管理)。
- 类型:
- SOCK_STREAM:面向连接(类似TCP),可靠字节流。
- SOCK_DGRAM:无连接(类似UDP),保留消息边界。
关键API与数据结构
地址结构体
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // 套接字文件路径(如 "/tmp/mysocket")
};
常用函数
函数 | 作用 | 与网络套接字的区别 |
---|---|---|
socket() | 创建套接字(AF_UNIX 域) | 协议族改为 AF_UNIX |
bind() | 绑定到文件系统路径 | 地址结构体为 sockaddr_un |
connect() | 连接服务端 | 目标地址为本地文件路径 |
unlink() | 删除套接字文件(清理时调用) | 网络套接字无需此操作 |
access/unlink
#include <unistd.h>
int access(const char *pathname, int mode);
/*
功能:判断给定的文件是否具有给的的权限
参数1:要被判断的文件描述符
参数2:要被判断的权限
R_OK:读权限
W_OK:写权限
X_OK:执行权限
F_OK:是否存在
返回值:如果要被判断的权限都存在,则返回0,否则返回-1并置位错误码
*/
int unlink(const char *path);
/*
功能:删除指定的文件
参数:要删除的文件路径
返回值:成功删除返回0,失败返回-1并置位错误码
*/
流式
int main(int argc, const char *argv[]) {
// 1、创建用于连接的套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
// 参数1:AF_UNIX表示使用的是ipv4的通信协议
// 参数2:SOCK_STREAM表示使用的是tcp通信
// 参数3:由于参数2指定了协议,参数3填0即可
if (sfd == -1) {
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd); //3
// 判断套接字文件是否存在
if (access("./unix", F_OK) == 0) {
// 说明文件已经存在,需要将其进行删除操作
if (unlink("./unix") == -1) {
perror("unlink error");
return -1;
}
}
// 2、绑定套接字文件描述符
// 2.1 填充要绑定的ip地址和端口号结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX; // 通信域
// sun.sun_path = "./unix"; //?不可以,因为字符串赋值需要使用 strcpy
strcpy(sun.sun_path, "./unix"); // 2.2 绑定工作
// 参数1:要被绑定的套接字文件描述符
// 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
// 参数3:参数2的大小
if (bind(sfd, (struct sockaddr*)&sun, sizeof(sun)) == -1) {
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、启动监听
// 参数1:要启动监听的文件描述符
// 参数2:挂起队列的长度
if (listen(sfd, 128) == -1) {
perror("listen error");
return -1;
}
printf("listen success\n");
// 4、阻塞等待客户端的连接请求
// 定义变量,用于接受客户端地址信息结构体
struct sockaddr_un cun; // 用于接收地址信息结构体的
socklen_t socklen = sizeof(cun); // 用于接收地址信息的长度
int newfd = accept(sfd, (struct sockaddr *)&cun, &socklen);
// 参数1:服务器套接字文件描述符
// 参数2:用于接收客户端地址信息结构体的容器,如果不接收,也可以填NULL
// 参数3:接收参数2的大小,如果参数2为NULL,则参数3也是NULL
if (newfd == -1) {
perror("accept error");
return -1;
}
printf("[%s]:已连接成功!!!!\n", cun.sun_path); // 输出套接字文件
// 5、数据收发
char rbuf[128] = ""; // 数据容器
while (1) {
// 清空容器中的内容
bzero(rbuf, sizeof(rbuf));
// 从套接字中读取消息
int res = recv(newfd, rbuf, sizeof(rbuf), 0);
if (res == 0) {
printf("对端已经下线\n");
break;
}
printf("[%s]:%s\n", cun.sun_path, rbuf);
// 对收到的数据处理一下,回给客户端
strcat(rbuf, "*_*");
// 将消息发送给客户端
if (send(newfd, rbuf, strlen(rbuf), 0) == -1) {
perror("send error");
return -1;
}
printf("发送成功\n");
}
// 6、关闭套接字
close(newfd);
close(sfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
int main(int argc, const char *argv[]) {
// 1、创建用于通信的客户端套接字文件描述符
int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket error");
return -1;
}
printf("socket success cfd = %d\n", cfd); //3
// 2、绑定ip地址和端口号(可选)
// 如果没有绑定,系统也不会自动绑定,会给的一个随机的文件
// 判断套接字文件是否存在
if (access("./linux", F_OK) == 0) {
// 说明文件已经存在,需要将其进行删除操作
if (unlink("./linux") == -1) {
perror("unlink error");
return -1;
}
}
// 2.1 填充要绑定的地址信息结构体
struct sockaddr_un cun;
cun.sun_family = AF_UNIX;
strcpy(cun.sun_path, "./linux");
// 2.2 绑定工作
if (bind(cfd, (struct sockaddr*)&cun, sizeof(cun)) == -1) {
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、连接服务器
// 3.1 填充要连接的服务器地址信息结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX; // 通信域
strcpy(sun.sun_path, "./unix"); // 服务器套接字文件
// 3.2 连接工作
if (connect(cfd, (struct sockaddr*)&sun, sizeof(sun)) == -1) {
perror("connect error");
return -1;
}
printf("连接服务器成功\n");
// 4、数据收发
char wbuf[128] = "";
while (1) {
// 清空容器
bzero(wbuf, sizeof(wbuf));
// 从终端获取数据
fgets(wbuf, sizeof(wbuf), stdin); //0
wbuf[strlen(wbuf)-1] = 0; // 将换行改成 '\0'
// 将数据发送给服务器
if (send(cfd, wbuf, sizeof(wbuf), 0) == -1) {
perror("send error");
return -1;
}
// 接受服务器发送过来的消息
if (recv(cfd, wbuf, sizeof(wbuf), 0) == 0) {
printf("对端已经下线\n");
break;
}
printf("收到服务器消息为:%s\n", wbuf);
}
// 5、关闭套接字
close(cfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}
报式
int main(int argc, const char *argv[]) {
// 1、创建用于通信的套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0); // SOCK_DGRAM表示基于udp通信方式
if (sfd == -1) {
perror("socket error");
return -1;
}
printf("socket success sfd = %d\n", sfd); //3
// 2、绑定套接字文件
// 判断套接字文件是否存在
if (access("./unix", F_OK) == 0) {
// 说明文件已经存在,需要将其进行删除操作
if (unlink("./unix") == -1) {
perror("unlink error");
return -1;
}
}
// 2.1 填充要绑定的ip地址和端口号结构体
struct sockaddr_un sun;
sun.sun_family = AF_UNIX; // 通信域
strcpy(sun.sun_path, "./unix"); // 套接字文件
// 2.2 绑定工作
// 参数1:要被绑定的套接字文件描述符
// 参数2:要绑定的地址信息结构体,需要进行强制类型转换,防止警告
// 参数3:参数2的大小
if (bind(sfd, (struct sockaddr*)&sun, sizeof(sun)) == -1) {
perror("bind error");
return -1;
}
printf("bind success\n");
// 3、数据收发
char rbuf[128] = "";
// 定义容器接收对端的地址信息结构体
struct sockaddr_un cun;
socklen_t socklen = sizeof(cun);
while (1) {
// 清空容器
bzero(rbuf, sizeof(rbuf));
// 从客户端中读取消息
if (recvfrom(sfd, rbuf, sizeof(rbuf), 0,
(struct sockaddr*)&cun, &socklen) == -1) {
perror("recvfrom error");
return -1;
}
printf("[%s]:%s\n", cun.sun_path, rbuf);
// 加个笑脸发给客户端
strcat(rbuf, "*_*");
// 将数据发送给客户端
sendto(sfd, rbuf, strlen(rbuf), 0,
(struct sockaddr*)&cun, sizeof(cun));
printf("发送成功\n");
}
// 4、关闭套接字
close(sfd);
std::cout << "Hello, World!" << std::endl;
return 0;
}