socket编程流程图(socket编程之常用api介绍与socket)
socket编程流程图(socket编程之常用api介绍与socket)网络字节序【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在qun文件里面,有需要的可以自行添加哦!(需要后台私信“1”自取)面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor6种epoll的设计,让你吊打面试官,而且他不能还嘴LinuxC 后台服务器开发架构师免费学习地址C/C Linux鏈嶅姟鍣ㄥ紑鍙�/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲层€�-瀛︿範瑙嗛鏁欑▼-鑵捐璇惧爞
前言本文旨在学习socket网络编程这一块的内容,epoll是重中之重,后续文章写reactor模型是建立在epoll之上的。
socket编程socket介绍传统的进程间通信借助内核提供的IPC机制进行 但是只能限于本机通信 若要跨机通信 就必须使用网络通信( 本质上借助内核-内核提供了socket伪文件的机制实现通信----实际上是使用文件描述符) 这就需要用到内核提供给用户的socket API函数库。
使用socket会建立一个socket pair,如下图, 一个文件描述符操作两个缓冲区。
使用socket的API函数编写服务端和客户端程序的步骤
面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor
6种epoll的设计,让你吊打面试官,而且他不能还嘴
LinuxC 后台服务器开发架构师免费学习地址C/C Linux鏈嶅姟鍣ㄥ紑鍙�/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲层€�-瀛︿範瑙嗛鏁欑▼-鑵捐璇惧爞
【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在qun文件里面,有需要的可以自行添加哦!(需要后台私信“1”自取)
网络字节序
网络字节序:大端和小端的概念
大端: 低位地址存放高位数据 高位地址存放低位数据
小端: 低位地址存放低位数据 高位地址存放高位数据
大端和小端的使用使用场合:在网络中经常需要考虑大端和小端的是IP和端口。网络传输用的是大端,计算机用的是小端 所以需要进行大小端的转换
下面4个函数就是进行大小端转换的函数,函数名的h表示主机host n表示网络network s表示short l表示long。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
上述的几个函数 如果本来不需要转换函数内部就不会做转换。
IP地址转换函数IP地址转换函数
int inet_pton(int af const char *src void *dst);
- p->表示点分十进制的字符串形式
- to->到
- n->表示network网络
函数说明: 将字符串形式的点分十进制IP转换为大端模式的网络IP(整形4字节数)
参数说明:
- af: AF_INET
- src: 字符串形式的点分十进制的IP地址
- dst: 存放转换后的变量的地址
- 例如inet_pton(AF_INET "127.0.0.1" &serv.sin_addr.s_addr);
手工也可以计算: 如192.168.232.145 先将4个正数分别转换为16进制数
192—>0xC0 168—>0xA8 232—>0xE8 145—>0x91
最后按照大端字节序存放: 0x91E8A8C0 这个就是4字节的整形值。
const char *inet_ntop(int af const void *src char *dst socklen_t size);
函数说明: 网络IP转换为字符串形式的点分十进制的IP 参数说明:
- af: AF_INET
- src: 网络的整形的IP地址
- dst: 转换后的IP地址 一般为字符串数组
- size: dst的长度
返回值:
- 成功–返回执行dst的指针
- 失败–返回NULL 并设置errno
例如: IP地址为010aa8c0 转换为点分十进制的格式:
01---->1 0a---->10 a8---->168 c0---->192
由于从网络中的IP地址是高端模式 所以转换为点分十进制后应该为: 192.168.10.1
struct sockaddrsocket编程用到的重要的结构体:struct sockaddr
//struct sockaddr结构说明:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
//struct sockaddr_in结构:
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 */
}; //网络字节序IP--大端模式
通过man 7 ip可以查看相关说明
主要API函数介绍socketint socket(int domain int type int protocol);
函数描述: 创建socket
参数说明:
- domain: 协议版本
- - AF_INET IPV4
- - AF_INET6 IPV6
- - AF_unix AF_LOCAL本地套接字使用
- type:协议类型
- - SOCK_STREAM 流式 默认使用的协议是TCP协议
- - SOCK_DGRAM 报式 默认使用的是UDP协议
- protocal:
- - 一般填0 表示使用对应类型的默认协议.
- 返回值:
- - 成功: 返回一个大于0的文件描述符
- - 失败: 返回-1 并设置errno
当调用socket函数以后 返回一个文件描述符 内核会提供与该文件描述符相对应的读和写缓冲区 同时还有两个队列 分别是请求连接队列和已连接队列(监听文件描述符才有,listenFd)
int bind(int sockfd const struct sockaddr *addr socklen_t addrlen);
函数描述: 将socket文件描述符和IP PORT绑定
参数说明:
- socket: 调用socket函数返回的文件描述符
- addr: 本地服务器的IP地址和PORT
struct sockaddr_in serv;
serv.sin_family = AF_INET;
serv.sin_port = htons(8888);
//serv.sin_addr.s_addr = htonl(INADDR_ANY);
//INADDR_ANY: 表示使用本机任意有效的可用IP
inet_pton(AF_INET "127.0.0.1" &serv.sin_addr.s_addr);
- addrlen: addr变量的占用的内存大小
返回值:
- 成功: 返回0
- 失败: 返回-1 并设置errno
int listen(int sockfd int backlog);
函数描述: 将套接字由主动态变为被动态
参数说明:
- sockfd: 调用socket函数返回的文件描述符
- backlog: 在linux系统中,这里代表全连接队列(已连接队列)的数量。在unix系统种,这里代表全连接队列(已连接队列) 半连接队列(请求连接队列)的总数
返回值:
- 成功: 返回0
- 失败: 返回-1 并设置errno
int accept(int sockFD struct sockaddr *addr socklen_t *addrlen);
函数说明:获得一个连接 若当前没有连接则会阻塞等待.
函数参数:
- sockfd: 调用socket函数返回的文件描述符
- addr: 传出参数 保存客户端的地址信息
- addrlen: 传入传出参数 addr变量所占内存空间大小
返回值:
- 成功: 返回一个新的文件描述符 用于和客户端通信
- 失败: 返回-1 并设置errno值.
accept函数是一个阻塞函数 若没有新的连接请求 则一直阻塞. 从已连接队列中获取一个新的连接 并获得一个新的文件描述符 该文件描述符用于和客户端通信. (内核会负责将请求队列中的连接拿到已连接队列中)
connectint connect(int sockfd const struct sockaddr *addr socklen_t addrlen);
函数说明: 连接服务器
函数参数:
- sockfd: 调用socket函数返回的文件描述符
- addr: 服务端的地址信息
- addrlen: addr变量的内存大小
返回值:
- 成功: 返回0
- 失败: 返回-1 并设置errno值
接下来就可以使用write和read函数进行读写操作了。除了使用read/write函数以外 还可以使用recv和send函数。
ssize_t read(int fd void *buf size_t count);
ssize_t write(int fd const void *buf size_t count);
ssize_t recv(int sockfd void *buf size_t len int flags);
ssize_t send(int sockfd const void *buf size_t len int flags);
//对应recv和send这两个函数flags直接填0就可以了
注意: 如果写缓冲区已满 write也会阻塞 read读操作的时候 若读缓冲区没有数据会引起阻塞。
高并发服务器模型-selectselect介绍多路IO技术: select 同时监听多个文件描述符 将监控的操作交给内核去处理
int select(int nfds fd_set * readfds fd_set *writefds fd_set *exceptfds struct timeval *timeout);
数据类型fd_set::文件描述符集合——本质是位图
函数介绍: 委托内核监控该文件描述符对应的读 写或者错误事件的发生
参数说明:
- nfds: 最大的文件描述符 1
- readfds: 读集合 是一个传入传出参数
传入: 指的是告诉内核哪些文件描述符需要监控
传出: 指的是内核告诉应用程序哪些文件描述符发生了变化
- writefds: 写文件描述符集合(传入传出参数,同上)
- execptfds: 异常文件描述符集合(传入传出参数,同上)
- timeout:
NULL--表示永久阻塞 直到有事件发生
0 --表示不阻塞 立刻返回 不管是否有监控的事件发生
>0 --到指定事件或者有事件发生了就返回
- 返回值: 成功返回发生变化的文件描述符的个数。失败返回-1 并设置errno值。
将fd从set集合中清除
void FD_CLR(int fd fd_set *set);
功能描述: 判断fd是否在集合中 返回值: 如果fd在set集合中 返回1 否则返回0
int FD_ISSET(int fd fd_set *set);
将fd设置到set集合中
void FD_SET(int fd fd_set *set);
初始化set集合
void FD_ZERO(fd_set *set);
用select函数其实就是委托内核帮我们去检测哪些文件描述符有可读数据 可写 错误发生
int select(int nfds fd_set * readfds fd_set *writefds fd_set *exceptfds struct timeval *timeout);
select优缺点
select优点:
- select支持跨平台
select缺点:
- 代码编写困难
- 会涉及到用户区到内核区的来回拷贝
- 当客户端多个连接 但少数活跃的情况 select效率较低(例如: 作为极端的一种情况 3-1023文件描述符全部打开 但是只有1023有发送数据 select就显得效率低下)
- 最大支持1024个客户端连接(select最大支持1024个客户端连接不是有文件描述符表最多可以支持1024个文件描述符限制的 而是由FD_SETSIZE=1024限制的)
FD_SETSIZE=1024 fd_set使用了该宏 当然可以修改内核 然后再重新编译内核 一般不建议这么做
select代码实现#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_LEN 4096
int main(int argc char **argv) {
int listenfd connfd n;
struct sockaddr_in svr_addr;
char buff[MAX_LEN];
if ((listenfd = socket(AF_INET SOCK_STREAM 0)) == -1) {
printf("create socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
memset(&svr_addr 0 sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if (bind(listenfd (struct sockaddr *) &svr_addr sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
if (listen(listenfd 10) == -1) {
printf("listen socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
//select
fd_set rfds rset wfds wset;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(listenfd &rfds);
int max_fd = listenfd;
while (1) {
rset = rfds;
wset = wfds;
int nready = select(max_fd 1 &rset &wset NULL NULL);
if (FD_ISSET(listenfd &rset)) { //
struct sockaddr_in clt_addr;
socklen_t len = sizeof(clt_addr);
if ((connfd = accept(listenfd (struct sockaddr *) &clt_addr &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
FD_SET(connfd &rfds);
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
int i = 0;
for (i = listenfd 1; i <= max_fd; i ) {
if (FD_ISSET(i &rset)) { //
n = recv(i buff MAX_LEN 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n" buff);
FD_SET(i &wfds);
}
else if (n == 0) { //
FD_CLR(i &rfds);
close(i);
}
if (--nready == 0) break;
}
else if (FD_ISSET(i &wset)) {
send(i buff n 0);
FD_SET(i &rfds);
FD_CLR(i &wfds);
}
}
}
close(listenfd);
return 0;
}
高并发服务器模型-pollpoll介绍
poll跟select类似 监控多路IO 但poll不能跨平台。其实poll就是把select三个文件描述符集合变成一个集合了。
int poll(struct pollfd *fds nfds_t nfds int timeout);
参数说明:
- fds: 传入传出参数 实际上是一个结构体数组
fds.fd: 要监控的文件描述符
fds.events:
POLLIN---->读事件
POLLOUT---->写事件
fds.revents: 返回的事件
- nfds: 数组实际有效内容的个数
- timeout: 超时时间 单位是毫秒.
-1:永久阻塞 直到监控的事件发生
0: 不管是否有事件发生 立刻返回
>0: 直到监控的事件发生或者超时
返回值:
- 成功:返回就绪事件的个数
- 失败: 返回-1。若timeout=0 poll函数不阻塞 且没有事件发生 此时返回-1 并且errno=EAGAIN 这种情况不应视为错误。
struct pollfd {
int fd; /* file descriptor */ 监控的文件描述符
short events; /* requested events */ 要监控的事件---不会被修改
short revents; /* returned events */ 返回发生变化的事件 ---由内核返回
};
说明:
- 当poll函数返回的时候 结构体当中的fd和events没有发生变化 究竟有没有事件发生由revents来判断 所以poll是请求和返回分离
- struct pollfd结构体中的fd成员若赋值为-1 则poll不会监控
3.相对于select poll没有本质上的改变; 但是poll可以突破1024的限制.在/proc/sys/fs/file-max查看一个进程可以打开的socket描述符上限 如果需要可以修改配置文件: /etc/security/limits.conf 加入如下配置信息 然后重启终端即可生效
soft nofile 1024
* hard nofile 100000
soft和hard分别表示ulimit命令可以修改的最小限制和最大限制
poll代码实现#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_LEN 4096
#define POLL_SIZE 1024
int main(int argc char **argv) {
int listenfd connfd n;
struct sockaddr_in svr_addr;
char buff[MAX_LEN];
if ((listenfd = socket(AF_INET SOCK_STREAM 0)) == -1) {
printf("create socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
memset(&svr_addr 0 sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if (bind(listenfd (struct sockaddr *) &svr_addr sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
if (listen(listenfd 10) == -1) {
printf("listen socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
//poll
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
int i = 0;
for (i = 1; i < POLL_SIZE; i ) {
fds[i].fd = -1;
}
while (1) {
int nready = poll(fds max_fd 1 -1);
if (fds[0].revents & POLLIN) {
struct sockaddr_in client = {};
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd (struct sockaddr *) &client &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
printf("accept \n");
fds[connfd].fd = connfd;
fds[connfd].events = POLLIN;
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
//int i = 0;
for (i = listenfd 1; i <= max_fd; i ) {
if (fds[i].revents & POLLIN) {
n = recv(i buff MAX_LEN 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n" buff);
send(i buff n 0);
}
else if (n == 0) { //
fds[i].fd = -1;
close(i);
}
if (--nready == 0) break;
}
}
}
}
高并发服务器模型-epoll(重点)epoll介绍
将检测文件描述符的变化委托给内核去处理 然后内核将发生变化的文件描述符对应的事件返回给应用程序。
记住,epoll是事件驱动的,其底层数据结构是红黑树,红黑树的key是fd,val是事件,返回的是事件。
epoll有两种工作模式,ET和LT模式。
水平触发LT:
- 高电平代表1
- 只要缓冲区中有数据 就一直通知
边缘触发ET:
- 电平有变化就代表1
- 缓冲区中有数据只会通知一次 之后再有新的数据到来才会通知(若是读数据的时候没有读完 则剩余的数据不会再通知 直到有新的数据到来)
epoll默认是水平触发LT,在需要高性能的场景下,可以改成边缘ET非阻塞方式来提高效率。
一般使用LT是一次性读数据读不完,数据较多的情况。而一次性能够读完,小数据量则用边缘ET。
ET模式由于只通知一次 所以在读的时候要循环读 直到读完 但是当读完之后read就会阻塞 所以应该将该文件描述符设置为非阻塞模式(fcntl函数)
read函数在非阻塞模式下读的时候 若返回-1 且errno为EAGAIN 则表示当前资源不可用 也就是说缓冲区无数据(缓冲区的数据已经读完了); 或者当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲区中已没有数据可读了,也就可以认为此时读事件已处理完成。
epoll反应堆反应堆: 一个小事件触发一系列反应
epoll反应堆的思想: c 的封装思想(把数据和操作封装到一起)
- 将描述符 事件 对应的处理方法封装在一起
- 当描述符对应的事件发生了 自动调用处理方法(其实原理就是回调函数)
poll反应堆的核心思想是: 在调用epoll_ctl函数的时候 将events上树的时候 利用epoll_data_t的ptr成员 将一个文件描述符 事件和回调函数封装成一个结构体 然后让ptr指向这个结构体。然后调用epoll_wait函数返回的时候 可以得到具体的events 然后获得events结构体中的events.data.ptr指针 ptr指针指向的结构体中有回调函数 最终可以调用这个回调函数。
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll-api
int epoll_create(int size);
函数说明: 创建一个树根
参数说明:
- size: 最大节点数 此参数在linux 2.6.8已被忽略 但必须传递一个大于0的数,历史意义,用epoll_create1也行。
- 返回值:
成功: 返回一个大于0的文件描述符 代表整个树的树根.
失败: 返回-1 并设置errno值.
int epoll_ctl(int epfd int op int fd struct epoll_event *event);
函数说明: 将要监听的节点在epoll树上添加 删除和修改
参数说明:
- epfd: epoll树根
- op:
EPOLL_CTL_ADD: 添加事件节点到树上
EPOLL_CTL_DEL: 从树上删除事件节点
EPOLL_CTL_MOD: 修改树上对应的事件节点
- fd: 事件节点对应的文件描述符
- event: 要操作的事件节点
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- event.events常用的有:
EPOLLIN: 读事件
EPOLLOUT: 写事件
EPOLLERR: 错误事件
EPOLLET: 边缘触发模式
- event.fd: 要监控的事件对应的文件描述符
int epoll_wait(int epfd struct epoll_event *events int maxevents int timeout);
函数说明:等待内核返回事件发生
参数说明:
- epfd: epoll树根
- events: 传出参数 其实是一个事件结构体数组
- maxevents: 数组大小
- timeout:
-1: 表示永久阻塞
0: 立即返回
>0: 表示超时等待事件
返回值:
成功: 返回发生事件的个数
失败: 若timeout=0 没有事件发生则返回; 返回-1 设置errno值
epoll_wait的events是一个传出参数 调用epoll_ctl传递给内核什么值 当epoll_wait返回的时候 内核就传回什么值 不会对struct event的结构体变量的值做任何修改。
epoll优缺点epoll优点:
- 性能高,百万并发不在话下,而select就不行
epoll缺点:
- 不能跨平台,linux下的
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
#define POLL_SIZE 1024
#define MAX_LEN 4096
int main(int argc char **argv) {
int listenfd connfd n;
char buff[MAX_LEN];
struct sockaddr_in svr_addr;
memset(&svr_addr 0 sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(8081);
if ((listenfd = socket(AF_INET SOCK_STREAM 0)) == -1) {
printf("create socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
if (bind(listenfd (struct sockaddr *) &svr_addr sizeof(svr_addr)) == -1) {
printf("bind socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
if (listen(listenfd 10) == -1) {
printf("listen socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
int epfd = epoll_create(1); //int size
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd EPOLL_CTL_ADD listenfd &ev);
while (1) {
int nready = epoll_wait(epfd events POLL_SIZE 5);
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0; i < nready; i ) {
int actFd = events[i].data.fd;
if (actFd == listenfd) {
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
if ((connfd = accept(listenfd (struct sockaddr *) &cli_addr &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n" strerror(errno) errno);
return 0;
}
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd EPOLL_CTL_ADD connfd &ev);
}
else if (events[i].events & EPOLLIN) {
n = recv(actFd buff MAX_LEN 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n" buff);
send(actFd buff n 0);
}
else if (n == 0) { //
epoll_ctl(epfd EPOLL_CTL_DEL actFd NULL);
close(actFd);
}
}
}
}
return 0;
}