网络套接字
在通信过程中,套接字一定成对出现
一个文件描述符指向一个套接字(该套接字由内核借助两个缓冲区实现)
预备知识
网络字节序
网络字节序是TCP/IP协议中规定的一种数据表示格式,它采用大端排序方式
小端法:高位存在高地址,低位存在低地址(计算机采用)
大端法:高位存在低地址,低位存在高地址(网络采用)
需要进行网络字节序和主机字节序的转换
uint32_t htonl(uint32_t hostlong);本地——>网络 IP
uint16_t htons(uint16_t hostshort);本地——>网络 端口
uint32_t ntohl(uint32_t netlong);网络——>本地 IP
uint16_t ntohs(uint16_t netshort);网络——>本地 端口
h:host n:network l:32位整型数 s:16位整型数
头文件:
#include <arpa/inet.h>
IP地址转换
int inet_pton(int af, const char *src, void *dst);点八十进制(string)——>二进制(int)
参数:
af:IP类型
AF_INET:IPv4
AF_INET6:IPv6
src:IP地址(点分十进制)
dst:传出参数 转换后的 网络字节序的 IP地址
返回:
成功:1
异常:0 说明src指向的不是一个有效的IP地址
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);二进制(int)——>点八十进制(string)
参数:
af:IP类型
AF_INET:IPv4
AF_INET6:IPv6
src:IP地址(二进制)
dst:传出参数 转换后的 本地字节序的 IP地址
size:dst的大小
返回:
成功:dst
失败:NUL
socketaddr地址结构
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* 端口的网络字节序 */
struct in_addr sin_addr; /* IP地址 */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* 网络字节序 */
};
示例:
struct socketadr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9527);
addr.sin_addr.s_addr = htonl(INADD_ANY); 取出系统中任意有效的IP地址,二进制类型
bind(fd, (struct socketadr)&addr, size)
网络套接字函数
一个服务器和一个客户端通信,一共有 3个套接字(2个用于服务器和客户端之间的连接,1个用于客户端监听)
socket函数
创建套接字
int socket(int domain, int type, int protocol);
参数:
domain:指定IP协议
AF_INET IPv4
AF_INET6 IPv6
AF_UNIX 本地套接字
type:数据传输协议
SOCK_STREAM 流式 TCP
SOCK_DGRAM 报式 UDP
protocol:选用协议的代表协议是什么
通常:0
返回值:
成功:新套接字所对应的文件描述符
失败:-1 errno
头文件:
#include <sys/socket.h>
示例:
socket(AF_INET, SOCK_STREAM, 0)
bind函数
给socket绑定一个地址结构(IP+端口号)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:socket函数返回值
addr:地址结构
struct socketadr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9527);
addr.sin_addr.s_addr = htonl(INADD_ANY);
addrlen:地址结构大小
sizeof(addr)
返回值:
成功:0
失败:-1 errno
listen函数
设置同时与服务器建立连接的上线数(同时进行3次握手的客户端数量)
int listen(int sockfd, int backlog);
参数:
sockfd:socket函数返回值
backlog:上限数值。最大值为128
返回值:
成功:0
失败:-1 errno
accept函数
阻塞等待客户端建立连接。成功,返回一个与客户端成功建立连接的socket文件描述符(新的)
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
参数:
socket:socket函数返回值
address:传出参数 成功与客户端成功建立连接的地址结构(IP+端口号)
address_len:传入传出 地址结构大小
返回值:
成功:能与服务器进行数据通信的socket的文件描述符
失败:-1 errno
connect函数
与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd:socket函数返回值
addr:传入参数 服务器的地址结构
struct sockaddr_in ser_addr; 服务器地址结构
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(SERV_PORT); 和服务器设置的端口相同
inet_pton(AF_INET,"127.0.0.1",&ser_addr.sin_addr.s_addr); 主机字节序 转 网络字节序
addrlen:服务器地址结构的长度
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构是可以的,采用“隐式绑定”
C/S模型的TCP通信结构
- 服务器端 server.cpp
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
using namespace std;
#define SERV_PORT 9527
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int fd,cfd;
struct sockaddr_in ser_addr,clit_addr;
socklen_t clit_addr_len;
const int bufferSize = 1024;
char buf[bufferSize];
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(SERV_PORT);
ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);
fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
sys_err("socket error");
}
bind(fd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
listen(fd,128);
clit_addr_len = sizeof(clit_addr);
cfd=accept(fd,(struct sockaddr*)&clit_addr,&clit_addr_len);
if(cfd == -1){
sys_err("accept error");
}
while(1){
int ret=read(cfd,&buf,sizeof(buf));
write(STDOUT_FILENO,&buf,ret);
for (ssize_t i = 0; i < ret; ++i) {
buf[i] = std::toupper(buf[i]);
}
write(cfd,&buf,ret);
}
close(fd);
close(cfd);
return 0;
}
- 客户端 client.cpp
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
using namespace std;
#define SERV_PORT 9527
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int cfd;
int ret;
struct sockaddr_in ser_addr;
const int bufferSize = 1024;
char buf[bufferSize];
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(SERV_PORT);
inet_pton(AF_INET,"127.0.0.1",&ser_addr.sin_addr.s_addr);
cfd = socket(AF_INET,SOCK_STREAM,0);
connect(cfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
while(1){
write(cfd,"hello",5);
ret=read(cfd,buf,bufferSize);
write(STDOUT_FILENO,buf,ret);
}
return 0;
}
UDP实现的C/S模型
recv()/send() 只能用于TCP通信。代替 read/write
recvfrom()/sendto() 用于UDP通信
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:套接字
buf:缓冲区
len:缓冲区大小
flags:0
src_addr:对端地址结构
(struct sockaddr *)&addr
addrlen:地址结构大小
返回值:
>0 接收数据字节数
0 对端关闭
-1 errno
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
参数:
sockfd:套接字
buf:存储数据的缓冲区
len:缓冲区大小
flags:0
dest_addr:对端地址结构
(struct sockaddr *)&addr
addrlen:地址结构大小
服务器端
lfd=socket(AF_INET,SOCK_DGRAM,0);
bind();
while(1){
recvfrom()
小-大
sendto();
}
close();
客户端:
connfc=socket(AF_INET,SOCK_DGRAM,0);
sendto(服务器地址结构,地址结构大小);
recvfrom();
close();
本地套接字(domain)
使用C/S模型实现本地进程通信
对比网络编程TCP C/S模型,注意:
1、int socket(int domain, int type, int protocol);
参数:
domain
AF_INET ——> AF_UNIX / AF_LOCAL
type
SOCK_STREAM / SOCK_DGRAM
2、绑定地址结构:sockaddr_in ——> sockaddr_un
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* 带有路径的文件名 */
};
struct sockaddr_un serv_addr;
serv_addr.sun_family=AF_UNIX;
strcpy(serv_addr.sun_path,"server.socket")
len=offsetof(struct sockaddr_un,serv_addr.sun_path)+strlen("server.socket")
unlink("server.socket");
bind(cfd,(struct sockaddr*)&serv_addr,len);
3、bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind 之前,可以使用 unlink("server.socket")
cfd=accept(lfd,(struct sockaddr*)&cli_addr,&len);
4、客户端:
struct sockaddr_un cli_addr;
cli_addr.sun_family=AF_UNIX;
strcpy(cli_addr.sun_path,"client.socket")
len=offsetof(struct sockaddr_un,cli_addr.sun_path)+strlen("client.socket")
unlink("client.socket");
bind(cfd,(struct sockaddr*)&cli_addr,len);
struct sockaddr_un serv_addr;
serv_addr.sun_family=AF_UNIX;
strcpy(serv_addr.sun_path,"server.socket")
len=offsetof(struct sockaddr_un,serv_addr.sun_path)+strlen("server.socket")
connect(cfd,(struct sockaddr*)&serv_addr,len);
出错处理函数封装
高并发服务器
多进程并发服务器
1. socket() 创建监听套接字 lfd
2. bind()
3. listen()
4. while(1){
cfd=accept();
pid=fork();
if(pid==0){ 子进程
close(lfd); 关闭用于建立连接的套接字
read(cfd);
write(cfd);
} else if(pid>0){ 父进程
close(cfd); 关闭用于通信的套接字
注册信号捕捉函数,在回调函数中回收子进程
continus;
}
}
多线程并发服务器
1. socket() 创建监听套接字 lfd
2. bind()
3. listen()
4. while(1){
cfd=accept();
tid=pthread_create();
close(cfd); 关闭用于通信的套接字
pthread_detach(tid); 回收线程;也可以:创建线程——专门用于回收线程
}
5. void *tfn(void *arg){
close(lfd); 关闭用于建立连接的套接字
read();
write();
}
多路I/O转接服务器
不再由应用程序自己监听客户端连接,取而代之的是内核监听
select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:select监听的最大的那个文件描述符+1
readfds:传入传出 读文件描述符监听集合
writefds:传入传出 写文件描述符监听集合 通常:NULL
exceptfds:传入传出 异常文件描述符监听集合 通常:NULL
timeout:超时时长
NULL:阻塞监听
设置timeval:等待固定时间
0:非阻塞监听,轮询
返回值:
成功:监听的三种事件发生的总个数
失败:-1 errno
void FD_ZERO(fd_set *set);置零
fd_set rset;
FD_ZERO(&fd_set);
void FD_SET(int fd, fd_set *set);将一个文件描述符添加入集合
FD_SET(3,&fd_set);FD_SET(5,&fd_set);FD_SET(6,&fd_set);
void FD_CLR(int fd, fd_set *set);将一个文件描述符从集合中清除出去
FD_CLR(3,&fd_set);
int FD_ISSET(int fd, fd_set *set);判断是否在集合中
头文件:
#include <sys/select.h>
select的优缺点:
缺点:
- 监听上限受文件描述符大小限制,最大1024
- 检测满足条件的id,需要自己添加业务逻辑提高效率(自定义处理数组)
优点:
- 跨平台
突破1024文件描述符限制:
- cat /proc/sys/fs/file-max 一个文件可以打开的socket描述符上限——>受硬件限制
- ulimit -a 查询当前进程默认打开文件描述符个数
select实现多路I/O转接
思路分析:
lfd=socket();
bind();
listen();
fd_set rset,allset;
FD_ZERO(&allset);
FD_SET(lfd,&allset);
while(1){
rset=allset; 保存监听集合
ret=select(lfd+1,&rest,NULL,NULL,NULL);
if(ret>0){ 有满足监听的描述符
if(FD_ISSET(lfd,&rset)){ 1 在 0 不在
cfd=accept(); 建立连接,返回用于通讯的文件描述符
FD_SET(cfd,&allset); 添加到监听集合中
}
for(i=lfd+1;i<=最大文件描述符;i++){
FD_ISSET(i,&rset); 有read、write事件
read();
...
write();
}
}
}
实现:
#include <bits/stdc++.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
using namespace std;
#define SERV_PORT 6666
void sys_err(const char *str){
perror(str);
exit(1);
}
int main(int argc,char *argv[]){
int i,j,n,nready;
int maxfd=0;
int listenfd,connfd;
char buf[BUFSIZ];
struct sockaddr_in clie_addr,serv_addr;
socklen_t clie_addr_len;
listenfd=socket(AF_INET,SOCK_STREAM,0);
//端口复用
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);
bind(listenfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
listen(listenfd,128);
fd_set rset,allset;
maxfd=listenfd;
FD_ZERO(&allset);
FD_SET(listenfd,&allset);
while(1){ //每次循环重新设置select监控集
rset=allset;
nready=select(maxfd+1,&rset,NULL,NULL,NULL);
if(nready<0)
sys_err("select error");
if(FD_ISSET(listenfd,&rset)){ //说明有新的客户端连接请求
clie_addr_len=sizeof(clie_addr);
connfd=accept(listenfd,(struct sockaddr*)&clie_addr,&clie_addr_len);
FD_SET(connfd,&allset);
if(maxfd < connfd)
maxfd=connfd;
if(--nready == 0) //表示只有listenfd一个描述符,不用执行for了
continue;
}
for(i=listenfd+1;i<=maxfd;i++){ //说明有其他数据请求
if(FD_ISSET(i,&rset)){
if((n=read(i,buf,sizeof(buf)))== 0){ //n==0 表示客户端关闭,服务器端也随之关闭
close(i);
FD_CLR(i,&allset); //解除对该描述符的监听
}else if (n > 0)
{
for (ssize_t i = 0; i < n; ++i) {
buf[i] = std::toupper(buf[i]);
}
write(i,&buf,n);
}
}
}
}
return 0;
}
epoll函数
能显著提高程序在大量并发连接中只有少量活跃的情况下系统CPU的利用率
int epoll_create(int size); 创建红黑树
参数:
size:创建的红黑树监听节点数(仅供内核参考)
返回值:
成功:指向新创建的红黑树的根节点
失败:-1,errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作红黑树
参数:
epfd:epoll_create的返回值
op:对该监听红黑树所作的操作
EPOLL_CTL_ADD:添加fd到红黑树
EPOLL_CTL_MOD:修改fd在红黑树上的监听事件
EPOLL_CTL_DEL:从红黑树上删除fd(取消监听)
fd:待监听的fd
event:本质是struct epoll_event结构体 需要epoll监视的fd对应的事件类型
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
uint32_t events:
EPOLLIN:读事件
EPOLLOUT:写事件
EPOLLERR:异常事件
EPOLLET:ET模式
成员 epoll_data_t :联合体(共用体) {
void *ptr;
int fd; 对应监听事件的fd
uint32_t u32;
uint64_t u64;
}
返回值:
成功:0
失败:-1,errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);阻塞监听
参数:
epfd:epoll_create的返回值
events:传出参数 满足监听条件的fd集合,类似于【数组】
maxevents:数组元素的总个数
timeout:超时时长
-1 阻塞
0 非阻塞
>0 设置时间(ms)
返回值:
成功:
>0 满足监听的总个数,可以用作循环下上限
0 没有满足的fd
失败:-1,errno
头文件:
#include <sys/epoll.h>
epoll实现多路IO转接
lfd=socket(); 监听连接事件lfd
bind();
listen();
int epfd=epoll_create(1024); 监听红黑树根节点epfd
struct epoll_event tep,ep[1024]; tep:用来设置单个fd属性
tep.events=EPOLLIN; 初始化lfd的监听属性
tep.data.fd=lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep) 将lfd添加到红黑树上
while(){
int ret=epoll_wait(epfd,ep,1024,-1) 实时监听
for(i=0;i<ret;i++){
if(ep[i].data.fd == lfd){ lfd满足读事件,有新的客户端连接请求
cfd=accept();
tep.events=EPOLLIN; 初始化cfd的监听属性
tep.data.fd=cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tep);
}else{ cfd们满足读事件,有客户端写数据
n=read();
if(n==0){ 客户端关闭,关闭服务器连接
close(ep[i].data.fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,ep[i].data.fd,NULL); 将关闭的cfd从红黑树摘下
}else if(n>0){
对应操作
write(ep[i].data.fd,buf,n);
}
}
}
}
epoll进阶
事件模型
EPOLL事件有两种模型:。
- Edge Triggered(ET):边缘触发,只有数据到来才触发,不管缓存区中是否还有数据Level
- Triggered(LT):水平触发,只要有数据都会触发。——默认采用的模式
ET
epoll的ET模式是 高速模式,但是只支持 非阻塞模式
struct epoll_event event;
event.events=EPOLLIN | EPOLLET;
flag=fcnt(connfd,F_GETFL); 修改connfd为非阻塞读
flag|=O_NONBLOCK;
fcnt(connfd,F_SETFL,flag);
event.data.fd=connfd;
LT
struct epoll_event event;
event.events=EPOLLIN;
epoll优缺点
优点:高效、能突破1024文件描述符
缺点:不能跨平台。Linux
epoll反应堆模型
epoll ET模式 + 非阻塞 + void *ptr
原来:socket、bind、listen —— epoll_create() 创建监听红黑树 —— 返回 epfd ——epoll_ctl() 添加一个监听fd —— epoll_wait() 阻塞监听 —— 对应监听描述符有事件产生 —— 返回 监听满足数组 —— 判断返回数组元素 —— lfd满足 —— accept() —— cfd满足 —— read() —— 小写转大写 —— write()回去
epoll反应堆:不但要监听cfd的读事件,还要监听cfd的写事件
socket、bind、listen —— epoll_create() 创建监听红黑树 —— 返回 epfd ——epoll_ctl() 添加一个监听fd —— epoll_wait() 阻塞监听 —— 对应监听描述符有事件产生 —— 返回 监听满足数组 —— 判断返回数组元素 —— lfd满足 —— accept() —— cfd满足 —— read() —— 小写转大写 ——cfd 从监听红黑树摘下 —— EPOLLOUT —— 回调函数 —— epoll_ctl() —— EPOLL_CTL_ADD 重新放到监听红黑树上监听写事件 —— 等待epoll_wait() 返回 —— 说明cfd可写 —— write()回去 —— cfd 从监听红黑树摘下 —— EPOLLIN —— epoll_ctl() —— EPOLL_CTL_ADD 重新放到监听红黑树上监听读事件 —— epoll_wait() 阻塞监听
线程池
线程池模块分析:
1、main()
创建线程池
向线程池添加任务,回调函数处理任务
销毁线程池
2、pthreadpool_create()
创建线程池结构体指针
初始化线程池结构体
创建N个任务线程
创建1个管理者线程
失败时,释放开辟的空间
3、threadpool_thread()
进入子线程回调函数
接收参数 void* arg——》pool 线程池
加锁——》线程池结构体锁
判断条件变量——》wait
4、管理者线程
循环10s执行一次
进入管理者线程回调函数
接收参数 void* arg——》pool 线程池
加锁——》线程池结构体锁
获取管理线程池要用到的变量:live_num busy_num task_num
根据既定算法使用三变量,判断是否应该创建、销毁线程池中的 指定步长的 线程
5、threadpool_add()
加锁——》线程池结构体锁
模拟产生任务process,使用回调函数处理任务
使用process的 回调函数 和 参数 初始化任务队列结构体成员
利用环形队列,实现添加任务。借助队尾指针挪移 % 实现
唤醒阻塞在条件变量上的线程
解锁——》线程池结构体锁
6、从3、wait之后处理任务
加锁——》线程池结构体锁
获取任务处理回调函数和参数
利用环形队列,实现处理任务。借助队头指针挪移 % 实现
唤醒阻塞在条件变量上的server
解锁——》线程池结构体锁
加锁——》记录忙状态线程个数的锁
改忙线程数++
解锁——》记录忙状态线程个数的锁
执行处理任务的线程
加锁——》记录忙状态线程个数的锁
改忙线程数–
解锁——》记录忙状态线程个数的锁
7、创建、销毁线程
管理者线程根据 task_num live_num,busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
如果满足 创建条件
pthread_create(),回调 任务线程函数。 live_num++
如果满足 销毁条件
wait_exit_thr_num=10;
signal给阻塞在条件变量上的线程,发送 假条件满足信号
跳转至 wait 阻塞线程会被 假信号 唤醒。wait_exit_thr_num>0 执行 pthread_exit()