Linux网络编程
网络基础
C/S模型、B/S模型
模型 | C/S | B/S |
---|---|---|
优点 | 缓存大量数据、协议选择灵活、速度快 | 安全性、跨平台、开发工作量较小 |
缺点 | 安全性、跨平台、开发工作量小 | 不能缓存大量数据、严格遵守http |
socket编程
套接字:socket本身有插座的意思,在Linux环境下,用来表示进程间网络通信的特殊文件类型。本质为内核借助缓存区形成的伪文件。
可以使用文件描述符引用套接字。与管道类似的,linux系统将其封装成文件的目的是统一接口,使得套接字操作和读写文件的操作一致。区别在于管道主要应用本地进程间通信,而套接字多应用于网络进程间数据的传递。
在网络通信过程中,套接字是成对存在的。
在TCP/IP协议中,“IP地址+TCP/UDP端口号”唯一标识网络通信中的一个进程。“IP地址+端口号”就对应一个socket。想建立连接的两个进程各自有一个socket来标识,那么这两个套接字组成的socket pair 就唯一标识一个连接。因此可以用套接字来描述网络连接的一对一关系。
套接字通信原理图:
一个文件操作符指向一个套接字(该套接字内部由内核借助两个缓存区实现)
。
网络字节序:
小端法:高位存高地址,低位存低地址-----计算机内部存储方式
大端法:高位存低地址,低位存高地址-----网络数据流
为使得网络程序具有可移植性,使同样c代码在大端和小端计算机上编译后都正常运行,可以调用库函数做网络字节序和主机字节序的转换
。
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);----本地转网络,ip
uint16_t htons(uint16_t hostshort);---本地转网络,port(端口)
uint32_t ntohl(uint32_t netlong);-----网络转本地,ip
uint16_t ntohs(uint_16_t netshort);---网络转本地,port
注释:
h--host,n--network,l--32位长整型,s--16位短整型
如果主机是小端字节序,这些函数将参数相应大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动返回。
常用ip是点分十进制的,本质是字符串,需要先转换为int,在转换htonl。
IP地址转换函数
int inet_pton(int af,const char* src,void *dst);---本地转网络
注释:
af:AF_INET,AF_INET6
src:IP地址(点分十进制)
dst:传出,转换后的网络字节序的IP地址
返回值:成功返回1,异常(不是有效的地址)返回0,失败返回-1
const char* inet_ntop(int af,const void*src,char* dst,socklent_t size);----网络转本地
注释:
af:AF_INET,AF_INET6
src:网络字节序的IP地址
dst:传出,转换后的IP地址(点分十进制)
size:dst的大小
返回值:成功返回dst,,失败返回NULL
sockaddr数据结构:
函数使用的是sockaddr,但是现在我们一般使用的是sockaddr_in,需要强制类型装换
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(250);
//1
int* dst;
inet_pton(AF_INET,"192.168.1.111",dst)
addr.sin_addr.s_addr = *dst;
//2 ----常用
addr.sin_addr.s_addr = htonl(INADDR_ANY);//自动获取系统中任意任意IP地址,二进制类型,在通过htonl转换。
bind(fd,(struct sockaddr *)&addr,size);/*因为函数内部使用的都是sockaddr,因此和socket相关的函数
都需要强转,sockaddr和sockaddr_in的大小是一样的,就是sockaddr_in的内部划分的更精细点。*/
-----使用man 7 ip查询到
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 */
};
socket模型流程图:
//一共三个套接字,服务端两个,一个通信,一个监听
服务端:
socket()创建套接字
bind()绑定IP+PORT
listen() 设置监听上限(同时)
accept()阻塞监听客户端连接,前面创建的套接字(监听)是传入参数,该函数成功后,会返回一个套接字(用于通信)
连接成功后,使用read()读数据,处理完后使用write()回数据。
客户端:
socket()创建套接字
connect()发起连接
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);----创建socket文件描述符
注释:
domain:AF_INET,AF_INET6,AF_UNIX
type:SOCK_STREAM,SOCK_DGRAM
protocol: 0----根据type默认选择(sock_stream对应tcp,sock_dgam对应udp)
返回值:成功返回新套接字所对应文件描述符,失败返回-1设置errno
服务端函数:
#include<arpa/ine.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);---->给socket绑定地址结构(IP+Port)
注释:
sockfd: socket()返回值
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(888);
addr.sin_adddr.s_addr = htonl(INADDR_ANY);
sockaddr: (struct sockaddr*)&addr
addrlen: sizeof(addr);
返回值:成功返回0,失败-1设置errno
int listen(int sockfd,int backlog); -----设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
注释:
sockfd: socket()返回值
backlog:上限数值,最大128
返回值:成功返回0,失败-1设置errno
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);-----阻塞等待客户端建立连接,成功返回一个与客户端成功连接的socket文件描述符
注释:
sockfd:socket()返回值
addr:传出参数,成功与服务器建立连接的那个客户端的地址结构(IP+Port)
struct sockaddr addr;
socklen_t clit_addr_len = sizeof(addr);
addrlen:传入addr的大小,传出客户端addr的实际大小
返回值:能与客户端进行通信的socket对应的文件描述符
客户端函数:
int connect(int sockfd,const sockaddr* addr,socklen_t addrlen);-----使用现有的socket与服务器建立连接。
注释;
sockfd:socket()返回值。
addr:传入参数,服务器的地址结构。
addrlen:服务器地址结构的大小。
返回值:成功0,失败返回-1设置errno
如果客户端不使用bind绑定客户端地址结构,采用“隐式绑定”
使用C/S模型实现大小写转换,即客户端读取用户输入小写字符串发送给服务端,服务端收到后,处理为大写,在发送给客户端,客户端终端显示。
步骤:
服务端:
1.socket() 创建socket
2.bind() 绑定服务器地址结构
3.listen() 设置监听上限
4.acccept() 阻塞监听客户端连接
5.read() 读socket获取客户端数据
6.toupper() 大小写转换实现
7.write() 写会给客户端
8.close() 关闭
客户端:
1.socket() 创建socket
2.connect() 与服务器建立连接
3.write() 写数据
4.read() 读转换后的数据
5.printf() 显示到终端
6.close() 关闭
***read函数的返回值:
>0:实际读到的字节数
=0:已经读到结尾(对端已经关闭)【重点】
-1:应该进一步判断errno的值:
EAGAIN或EWOULDBLOCK:设置了非阻塞模式读。没有数据到达。
EINTR:慢速系统调用被中断。
其他情况:异常
//服务器端
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
void sys_error(const char *str){
perror(str);
exit(1);
}
int main(int argc, char const *argv[])
{
int lfd,cfd;
char buf[4096],client_ip[1024];
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) sys_error("socket error");
struct sockaddr_in serv_addr,client_addr;
socklen_t client_addr_len = sizeof(client_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8888);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) sys_error("bind error");
if(listen(lfd,128)==-1) sys_error("listen error");
if((cfd = accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len))==-1) sys_error("accept error");
printf("client to:%s,port:%d",inet_ntop(AF_INET,&(client_addr.sin_addr.s_addr),client_ip,sizeof(client_ip)),ntohs(client_addr.sin_port));
while(1){
int ret = read(cfd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,ret);
for(int i = 0;i<ret;i++){
buf[i] = toupper(buf[i]);
}
write(cfd,buf,ret);
}
close(lfd);
close(cfd);
return 0;
}
//客户端
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
void sys_error(const char *str){
perror(str);
exit(1);
}
int main(int argc, char const *argv[])
{
int lfd;
char buf[4096];
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
inet_pton(AF_INET,"127.0.0.1",&(server_addr.sin_addr.s_addr ));
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd==-1) sys_error("socket error");
int ret = connect(lfd,(struct sockaddr*)(&server_addr),sizeof(server_addr));
printf("connect succeed");
if(ret != 0) sys_error("connect error");
while(1){
ret = read(STDIN_FILENO,buf,sizeof(buf));
write(lfd,buf,ret);
ret = read(lfd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,ret);
sleep(1);
}
close(lfd);
return 0;
}
高并发服务器
多进程并发服务器
//服务端
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<signal.h>
#include<sys/wait.h>
#include <errno.h>
void sys_error(const char *str){
perror(str);
exit(1);
}
void sig_catch(int signum){
while((waitpid(0,NULL,WNOHANG)) >0);
return;
}
int main(int argc, char const *argv[])
{
int lfd,cfd;
char buf[4096],client_ip[1024];
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) sys_error("socket error");
struct sockaddr_in serv_addr,client_addr;
bzero(&serv_addr,sizeof(serv_addr));
socklen_t client_addr_len = sizeof(client_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8889);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) sys_error("bind error");
if(listen(lfd,128)==-1) sys_error("listen error");
while(1){
cfd = accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len) ;
if(cfd == -1) {
if (errno == EINTR)
continue;
else
sys_error("accept error");
}
pid_t pid = fork();
if(pid==0){
close(lfd);
break;
}
close(cfd);
struct sigaction act;
act.sa_handler = sig_catch;//设置捕捉函数
sigemptyset(&act.sa_mask);//设置屏蔽字(捕捉函数期间生效)
act.sa_flags = 0;//设置默认属性
int ret = sigaction(SIGCHLD,&act,NULL);
if(ret == -1){
sys_error("sigaction error");
}
}
printf("client to:%s,port:%d\n",inet_ntop(AF_INET,&(client_addr.sin_addr.s_addr),client_ip,sizeof(client_ip)),ntohs(client_addr.sin_port));
while(1){
int ret = read(cfd,buf,sizeof(buf));
if(ret==0) break;
write(STDOUT_FILENO,buf,ret);
for(int i = 0;i<ret;i++){
buf[i] = toupper(buf[i]);
}
write(cfd,buf,ret);
}
close(cfd);
return 0;
}
多线程并发服务器
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<sys/wait.h>
#include <errno.h>;
#include<fcntl.h>
#define MAXLINE 8192
#define SERV_PORT 8000
struct s_info{//该结构体将地址和cfd捆绑
struct sockaddr_in cliaddr;
int connfd;
};
void sys_error(const char *str){
perror(str);
exit(1);
}
void* do_work(void *arg){
int n,i;
struct s_info* ts = (struct s_info*)arg;
char buf[INET_ADDRSTRLEN];
char str[INET_ADDRSTRLEN];
while (1)
{
n = read(ts->connfd,buf,MAXLINE);
if(n == 0){
printf("the client %d closed..\n",ts->connfd);
break;
}
printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&(*ts).cliaddr.sin_addr.s_addr,str,sizeof(str)),ntohs((*ts).cliaddr.sin_port));
for(i=0;i<n;i++){
buf[i] = toupper(buf[i]);
}
write(STDOUT_FILENO,buf,n);
write(ts->connfd,buf,n);
}
close(ts->connfd);
}
int main(int argc, char const *argv[])
{
int lfd,cfd;
pthread_t tid;
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) sys_error("socket error");
//端口复用
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
struct sockaddr_in serv_addr,client_addr;
bzero(&serv_addr,sizeof(serv_addr));
socklen_t client_addr_len = sizeof(client_addr);
struct s_info ts[256];
int i = 0;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8889);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(lfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) sys_error("bind error");
if(listen(lfd,128)==-1) sys_error("listen error");
printf("accepting client connect...\n");
while(1){
cfd = accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len) ;
if(cfd == -1) {
if (errno == EINTR)
continue;
else
sys_error("accept error");
}
ts[i].cliaddr = client_addr;
ts[i].connfd = cfd;
pthread_create(&tid,NULL,do_work,(void*)&ts[i]);
pthread_detach(tid);
i++;
}
close(lfd);
return 0;
}
netstat -apn | grep port ----可以查询端口的状态
端口复用
情景:先关闭服务器,在关闭客户端,服务器会处于TIME_WAIT状态,在此启动,会出现无法启动状态(需要等待2MSL)
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);----这个功能很复杂的
端口复用使用方法:
int opt = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
半关闭
通信双方,只有一端关闭通信---FIN_WAIT_2
close(cfd);
shutdown(int fd,int how);
注释:
how:SHUT_RD\SHUT_WR\SHUT_RDWR
两者区别:
文件描述符3指向某个文件,调用dup2(3,4),使得文件描述符4也指向3指向的文件,使用close(3),只会将文件描述符的引用计数-1,并不是关闭连接只有当引用计数为0时才关闭。但是使用shutdown(3,anyone);不考虑引用计数,直接关闭文件描述符。会把所有指向该文件的都关闭(3,4都不能在进行访问,而且可以通过参数2指定读写)。
多路IO转接服务器
多路IO转接服务器也叫多任务IO服务器。该类服务器实现的主旨思想是,不再由应用程序自己监听客户端连接,取而代之由内核代替监听。主要使用方法有三种。IO复用
select
内核提供
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
注释:
nfds:监听的文件描述符的最大+1,(从3开始(0,1,2被占用了))
readfds:读文件描述符集合,传入传出参数,读事件
writefds:写文件描述符集合,传入传出参数,写事件
exceptfds:异常文件描述符集合,传入传出参数,异常事件
timeout:超时参数,三种情况:
NULL:永久等待,阻塞监听;
设置timeval,等待固定时间;
设置timeval里时间均为0,检查描述字后立即返回,轮询。非阻塞监听+轮询
返回值:
>0:所有监听集合中满足对应事件(读写异常)的总数;
=0:没有满足监听要求的文件描述符;
-1:异常,errno;
void FD_CLR(int fd,fd_set* set);//将文件描述符从监听集合中剔除
int FD_ISSET(int fd,fd_set* set);//判断文件描述符是否在监听集合中
void FD_SET(int fd,fd_set* set);//将待监听的文件描述符添加到监听集合中。
void FD_ZERO(fd_set* set);//清空一个文件描述符集合
fd_set rset;
FD_ZERO(&rset);
FD_SET(3,&set);
FD_SET(3,&set);
思路分析:
lfd = socket();--创建套接字
bind();--绑定地址结构
listen();--设置监听上限
fd_set rset,allset;---创建监听集合
FD_ZERO(&allset);--清空监听集合
FD_SET(lfd,&allset);--将lfd加入读监听集合
while(1){
rset = allset;--保存监听集合
int ret = select(最大值+1,&rset,NULL,NULL,NULL);--监听 文件描述符集合
if(ret>0){
if(FD_ISSET(lfd,&rset)){--rset是传入传出参数,此刻含义已经变了。1表示在,0表示不在。
cfd = accept();--建立连接,返回用于通信的文件描述符
FD_SET(cfd,&allset);---监听通信
}
for(int i = lfd+1;i<=最大值;i++){
FD_ISSET(i,&rset);
read();
小---大
write();
}
}
}
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<sys/wait.h>
#include <errno.h>
#include<fcntl.h>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char const *argv[])
{
int n,nready;
int lfd,cfd;
int maxfd = 0;
char buf[1024];
struct sockaddr_in client_addr,server_addr;
socklen_t client_addr_len = sizeof(client_addr);
lfd = socket(AF_INET,SOCK_STREAM,0);
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8889);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(sockaddr*)&server_addr,sizeof(server_addr));
listen(lfd,128);
fd_set rset,all_set;//定义读集合,备份集合
vector<int> read_vec;//为了避免无效循环,但是没有解决关闭问题,应该用set。
maxfd = lfd;//最大文件描述符
FD_ZERO(&all_set);//清空
FD_SET(lfd,&all_set);//将待监听fd添加到监听集合
read_vec.push_back(lfd);
while (1)
{
rset = all_set;
nready = select(maxfd+1,&rset,NULL,NULL,NULL);//select监听
if(nready<0){
perror("select error");
exit(1);
}
if(FD_ISSET(lfd,&rset)){//lfd满足监听的读事件
cfd = accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len);//建立链接,不会阻塞
FD_SET(cfd,&all_set);//将新产生的fd添加进监听集合中,监听数据读事件
read_vec.push_back(cfd);
maxfd = maxfd>cfd?maxfd:cfd;//记录最大文件描述符
if(0==--nready) continue;
}
//for(int i = lfd+1;i<=maxfd;i++){//处理满足读事件的文件描述符(这里不好一点,可能就监听一个最大的,但是需要从头开始遍历,耗时)
for(int i:read_vec)
if(FD_ISSET(i,&rset)){//判断
if((n=read(i,buf,sizeof(buf))) == 0 ){//检测客户端是否关闭连接
close(i);
FD_CLR(i,&all_set);//移除已经关闭的文件描述符
}else if(n>0){
for(int j=0;j<n;j++){
buf[j] = toupper(buf[j]);
}
write(i,buf,n);
}
}
}
}
return 0;
}
select优缺点:
缺点:
1. 监听上限受文件描述符限制。最大1024;
2. 检测满足条件的fd,自己添加业务逻辑提高小。提高了编码难度
优点:
1.跨平台
poll
一般,相较于select改进不大。select 和 poll 系统调用的本质一样,poll() 的机制与 select() 类似,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制
(但是数量过大后性能也是会下降)。poll和 select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
注释:
fds:监听的文件描述符【结构体数组】
struct pollfd{
int fd;//待监听文件描述符
short events;//对应的监听事件,取值POLLIN,POLLOUT,POLLERR等
short revents;//传入时,给0.如果满足对应事件的话,返回非0---> POLLIN,POLLOUT,POLLERR等
};/*每个结构体的 events 由用户来设置,告诉内核我们关注的是什么,
而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件*/
nfds:监听数组的实际有效监听个数
timeout:
>0:超时时长。单位:毫秒。
=-1:阻塞等待。
=0:不阻塞
返回值:成功时:返回满足对应监听事件的总个数。如果在超时前没有任何事件发生,返回 0;
失败时:返回-1,并设置errno。
struct pollfd pfds[1024];
int size = 0;
pfds[0].fd = lfd;
pfds[0].events=POLLIN;
pfds[0].revents=0;
size++;
...
while(1){
int ret = poll(pfds,size,-1);
if(ret<0){
perror("select error");
}
for(int i = 0;i<size;i++){
if(pfds[i].renents&POLLIN){
....
}
}
}
//IO复用--poll
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<sys/wait.h>
#include <errno.h>
#include<fcntl.h>
#include<unordered_set>
using namespace std;
void sys_error(const char *str){
perror(str);
exit(1);
}
int main(int argc, char const *argv[])
{
int lfd,cfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd == -1) sys_error("socket error");
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
sockaddr_in sin;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_family = AF_INET;
sin.sin_port = htons(8888);
int ret = bind(lfd,(sockaddr*)&sin,sizeof(sin));
if(ret == -1) sys_error("bind error");
ret = listen(lfd,128);
struct pollfd pfds[1024];
int size = 0;
pfds[0].fd = lfd;
pfds[0].events = POLLRDNORM;
size++;
char buf[4096];
while(1){
ret = poll(pfds,size,-1);
if(ret == -1) sys_error("poll error");
for(int i=0;i<size;i++){
if(pfds[i].revents&POLLRDNORM){
if(i==0){//是lfd,建立连接
cfd = accept(lfd,NULL,NULL);
if(cfd == -1) sys_error("accept error");
if(size == 1024) sys_error("too many clients");
pfds[size].fd = cfd;
pfds[size].events = POLLRDNORM;
size++;
printf("client connect\n");
if(--ret == 0) break;
}else{
int r = read(pfds[i].fd,buf,sizeof(buf));
if(r<0){
if(errno = ECONNRESET){
close(pfds[i].fd);
if(i != size-1) pfds[i] = pfds[size-1];
size--;
}else
sys_error("read error");
}
else if(r == 0) {
close(pfds[i].fd);
if(i != size-1) pfds[i] = pfds[size-1];
size--;
}
else{
for(int j=0;j<r;j++){
buf[j] = toupper(buf[j]);
}
write(pfds[i].fd,buf,r);
}
if(--ret == 0) break;
}
}
}
}
return 0;
}
epoll
epoll是Linux下多路IO接口select/poll的增强版本,它可以显著提高程序在大量并发连接中只有少量活跃的情况下CPU的利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备被监听的文件描述符集合,另一个原因就是获取事件的时候,它无需遍历整个被监听的描述符集,只要遍历哪些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
目前epoll是linux大规模并发网络编程中的热门首选模型。
epoll除了提供select/poll那种IO事件的电平触发外,还提供了边沿触发,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高了应用程序效率。
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
可以使用cat命令查看一个进程可以打开的socket描述符的上限。
cat /proc/sys/fs/file-max
可以通过配置文件的方式修改上限值。
sudo vim /etc/security/limits.conf
在文件尾部写入以下配置,soft软限制,hard硬限制。如下所示
soft nofile 65536
hard nofile 100000
---------貌似修改了也没有用
epoll事件模型:
ET模式--边沿触发(缓存区剩余未读尽的数据不会导致epoll_wait返回,新的事件满足,才会触发)
LT模式--水平触发(默认模式)(缓存区剩余未读尽的数据会导致epoll_wait返回)
现象:对应管道而言(epoll不仅可以用在select上),子进程每5秒写10个数据,而父进程每次读5个数据,
对于水平触发,对于管道中还有数据的情况,epoll_wait也会触发,即父进程不间断读5个数据。
而对于边沿触发,父进程一次读5个数据,下一次只能等到子进程在写入10个数据时,epoll_wait才会触发。
结论:epoll的ET模式,高效模式,但是只支持非阻塞模式
int flag = fcntl(cfd,F_GETFL);
flag|= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
tep.events=EPOLLIN|EPOLLET;
tep.data.fd = cfd;
res=epoll_ctl(efd,EPOLL_CTL_ADD,cfd,&tep);
#include<sys/epoll.h>
int epoll_create(int size);----创建红黑树
注释:
size:创建的红黑树的监听节点数量(仅供参考)
返回值:成功返回指向新创建的红黑树的根节点的fd,失败返回-1设置errno
int epoll_ctl(int eptd,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:
typedef union epoll_data {
void *ptr;
int fd;//对应监听事件的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 */
};
其中events可以取EPOLLIN,EPOLLOUT,EPOLLERR等。
返回值:成功0,失败-1设置errno
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);----阻塞监听
注释:
epfd:epoll_create()的返回值
event:【数组】,传出参数,传出满足监听条件的结构体。(epoll_wait传出的数组)
maxevents:传入参数,数组元素的总个数。
timeout:
-1:阻塞
0:不阻塞
>0:超时时间
返回值:
>0:满足监听的总个数。可以用作循环上限
=0:无满足监听事件的fd
=-1:异常
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<sys/wait.h>
#include <errno.h>
#include<fcntl.h>
#include<sys/epoll.h>
#define OPEN_MAX 5000
#define MAXLINE 8192
#define SERV_PORT 8889
int main(int argc, char const *argv[])
{
int i,j,lfd,cfd,sfd;
int n,num=0;
ssize_t nready,efd,res;
char buf[MAXLINE],str[INET_ADDRSTRLEN];
socklen_t clilen;
struct sockaddr_in client_addr,server_addr;
socklen_t client_addr_len = sizeof(client_addr);
lfd = socket(AF_INET,SOCK_STREAM,0);
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8889);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(lfd,(sockaddr*)&server_addr,sizeof(server_addr));
listen(lfd,128);
struct epoll_event tep,ep[OPEN_MAX];
efd = epoll_create(OPEN_MAX);
if(efd == -1){
perror("epoll_create error");
exit(1);
}
tep.events = EPOLLIN;
tep.data.fd = lfd;
res = epoll_ctl(efd,EPOLL_CTL_ADD,lfd,&tep);
if(res==-1){
perror("epoll_ctl error");
exit(1);
}
while (1)
{
nready = epoll_wait(efd,ep,OPEN_MAX,-1);
if(nready == -1){
perror("epoll_wait error");
exit(1);
}
for(i=0;i<nready;i++){
if(!(ep[i].events&EPOLLIN) ) continue;
if(ep[i].data.fd == lfd){
cfd = accept(lfd,(struct sockaddr*)&client_addr,&client_addr_len);
printf("received from %s at PORT %d\n",inet_ntop(AF_INET,&client_addr.sin_addr,str,sizeof(str)),ntohs(client_addr.sin_port));
printf("cfd %d---client %d\n",cfd,++num);
// int flag = fcntl(cfd,F_GETFL);
// flag|= O_NONBLOCK;
// fcntl(cfd,F_SETFL,flag);
tep.events=EPOLLIN|EPOLLET;
tep.data.fd = cfd;
res=epoll_ctl(efd,EPOLL_CTL_ADD,cfd,&tep);
if(res==-1){
perror("epoll_ctl error");
exit(1);
}
}else{
sfd = ep[i].data.fd;
n = read(sfd,buf,MAXLINE);//非阻塞模式下这里需要轮询
if(n == 0 ){
res=epoll_ctl(efd,EPOLL_CTL_DEL,sfd,NULL);
if(res==-1){
perror("create_ctl error");
exit(1);
}
close(sfd);
printf("client %d closed connection\n",sfd );
}else if(n>0){
for(j=0;j<n;j++){
buf[j] = toupper(buf[j]);
}
write(sfd,buf,n);
write(STDOUT_FILENO,buf,n);
}
}
}
}
close(lfd);
return 0;
}
epoll反应堆模型
epoll ET模型 + 非阻塞、轮询 + void* ptr
流程:
socket(),bind(),listen();
epoll_create()创建红黑树
epoll_ctl()向树上添加一个监听fd
while(1){
epoll_wait()阻塞监听,满足监听时间,得到满足监听的数组和大小
if(lfd满足)---accept()函数
if(cfd满足)---read()函数--处理函数--cfd从监听树上摘下--EPOLLOUT--回调函数-重新放到红黑树上监听写事件---等待epoll_wait返回--说明cfd可写---write回去--cfd从监听树上摘下--EPOLLIN--epoll_ctl()--重新放到红黑树上---epoll_wait()监听读事件(到循环开头)。
}
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include<pthread.h>
#include <arpa/inet.h>
#include<signal.h>
#include <ctype.h>
#include<sys/wait.h>
#include <errno.h>
#include<fcntl.h>
#include<sys/epoll.h>
#define MAX_EVENTS 1024
#define BUFLEN 4096
#define SERV_PORT 8889
void recvdata(int fd,int events,void* arg);
void senddata(int fd,int events,void* arg);
//描述就绪文件描述符相关信息
struct myevent_s{
int fd;//要监听的文件描述符
int events;//对应的监听事件
void* arg;//泛型参数
void (*callback)(int fd,int events,void* arg);//回调函数
int status;//是否在监听:1--监听,0--不监听
char buf[BUFLEN];
int len;
long last_active;//记录每次加入红黑树g_efd的时间值
};
int g_efd;//保存epoll_create 返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1];//自定义结构体数组,+1--lfd
//将结构体myevent_成员变量初始化
void eventset(struct myevent_s* ev,int fd,void(*call_back)(int,int,void*),void* arg){
ev->fd=fd;
ev->callback = call_back;
ev->events=0;
ev->arg=arg;
ev->status=0;
/*这两行必须得注释,虽然有没有清理的问题,即关闭一个连接后,
又打开一个连接的话,可能有上一次的数据保存在buf中,但读数据的时候会覆盖,
以及记录新的数据长度,在使用时也不会出错。但是如果不注释这两行,每次读完,就会清空,发送的时候就没有数据了。*/
//memset(ev->buf,0,sizeof(ev->buf));
//ev->len=0;
ev->last_active=time(NULL);//调用eventset函数的时间
return;
}
//向epoll监听的红黑树添加一个文件描述符
void eventadd(int efd,int events,struct myevent_s *ev){
struct epoll_event epv={0,{0}};
int op;
epv.data.ptr=ev;
epv.events=ev->events=events;//EPOLLIN,EPOLLOUT
if(ev->status==0){
op = EPOLL_CTL_ADD;//将其加入红黑树g_efd,并将status置1
ev->status=1;
}
if(epoll_ctl(efd,op,ev->fd,&epv)<0){
printf("event add failed [ fd=%d],events [%d]\n",ev->fd,events);
}else{
printf("event add OK [ fd=%d],events [%d]\n",ev->fd,events);
}
}
//从epoll监听的红黑树删除一个文件描述符
void eventdel(int efd,struct myevent_s* ev){
struct epoll_event epv={0,{0}};
if(ev->status != 1) return;
epv.data.ptr = NULL;
ev->status = 0;//修改状态,就可以在下次有连接的时候,覆盖此位置。在acceptconn函数的for循环那里
epoll_ctl(efd,EPOLL_CTL_DEL,ev->fd,&epv);
return;
}
void recvdata(int fd,int events,void* arg){
struct myevent_s *ev = (struct myevent_s*)arg;
int len;
len = recv(fd,ev->buf,sizeof(ev->buf),0);
eventdel(g_efd,ev);
if(len>0){
ev->len = len;
ev->buf[len] = '\0';//手动添加字符串结束标记,这里其实应该是要发送的数据,该代码的功能是小写转大写(在发送时实现),所以这里就没做改变。
printf("[fd=%d] recv :[%s],len = %d \n",fd,ev->buf,len);
eventset(ev,fd,senddata,ev);//将fd的回调函数改为recvdata
eventadd(g_efd,EPOLLOUT,ev);//重新添加到红黑树上,设置监听读时间
}else if(len==0){
close(ev->fd);
printf("[fd=%d] pos[%ld],closed\n",fd,ev-g_events);
}
else{
close(ev->fd);
printf("recv[fd=%d] error %s\n",fd,strerror(errno));
}
return;
}
void senddata(int fd,int events,void* arg){
struct myevent_s *ev = (struct myevent_s*)arg;
int len;
for(int j=0;j<ev->len;j++){
ev->buf[j] = toupper(ev->buf[j]);
}
len = send(fd,ev->buf,ev->len,0);
eventdel(g_efd,ev);
if(len>0){
printf("send[fd=%d],[%d]%s\n",fd,len,ev->buf);
eventset(ev,fd,recvdata,ev);//将fd的回调函数改为recvdata
eventadd(g_efd,EPOLLIN,ev);//重新添加到红黑树上,设置监听读时间
}else{
close(ev->fd);
printf("send[fd=%d] error %s\n",fd,strerror(errno));
}
return;
}
//当有文件描述符就绪,epoll返回,调用该函数与客户端建立链接
void acceptconn(int lfd,int events,void *arg){
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd,i;
if((cfd=accept(lfd,(struct sockaddr*)&cin,&len))==-1){
if(errno != EAGAIN && errno != EINTR){
/* 暂时不做出错处理 */
}
printf("%s: accept,%s\n",__func__,strerror(errno));
return;
}
do{
for(i=0;i<MAX_EVENTS;i++){
if(g_events[i].status == 0) break;//找到一个没有被用的索引,添加进去
}
if(i == MAX_EVENTS){
printf("%s: max connect limit[%d]\n",__func__,MAX_EVENTS);//说明已经满了
break;
}
int flag = 0;//没什么用,可以不用记录
if((flag=fcntl(cfd,F_SETFL,O_NONBLOCK))<0){//设置非阻塞模式
printf("%s:fcntl nonblocking failed,%s\n",__func__,strerror(errno));
break;
}
//给cfd设置一个myevent_s结构体,回调函数,设置recvdata
eventset(&g_events[i],cfd,recvdata,&g_events[i]);
eventadd(g_efd,EPOLLIN|EPOLLET,&g_events[i]);
}while (0);
printf("new connect [%s:%d][time:%ld],pos[%d]\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port),g_events[i].last_active,i);
return;
}
//创建socket,初始化lfd
void initlistensocket(int efd,short port){
struct sockaddr_in sin;
int lfd = socket(AF_INET,SOCK_STREAM,0);
fcntl(lfd,F_SETFL,O_NONBLOCK);//设置非阻塞
memset(&sin,0,sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
bind(lfd,(struct sockaddr*)&sin,sizeof(sin));
listen(lfd,20);
eventset(&g_events[MAX_EVENTS],lfd,acceptconn,&g_events[MAX_EVENTS]);//将lfd放在g_events的索引MAX_EVENTS上
eventadd(efd,EPOLLIN|EPOLLET,&g_events[MAX_EVENTS]);
return;
}
int main(int argc, char const *argv[])
{
unsigned short port = SERV_PORT;
if(argc==2){
port = atoi(argv[1]);//使用用户指定端口
}
g_efd = epoll_create(MAX_EVENTS+1);
if(g_efd<=0){
printf("create efd in %s err %s\n",__func__,strerror(errno));
}
initlistensocket(g_efd,port);//初始话监听socket
struct epoll_event events[MAX_EVENTS+1];//保存已经满足就绪条件的文件描述符数组
printf("server running : port[%d]\n",port);
int checkpos = 0;
while(1){
//超时验证,每次测试100个链接,不测试lfd 当客户端60s内没有和服务器通信,则关闭此链接
long now = time(NULL);
for(int i = 0;i<100;i++,checkpos++){
if(checkpos == MAX_EVENTS) checkpos=0;
if(g_events[checkpos].status != 1) continue;
long duration = now -g_events[checkpos].last_active;
if(duration >= 60){
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n",g_events[checkpos].fd);
eventdel(g_efd,&g_events[checkpos]);
}
}
//监听红黑树g_efd,将满足事件的描述符加到events数组中,1s没有事件满足,返回0
int nfd = epoll_wait(g_efd,events,MAX_EVENTS+1,1000);
if(nfd<0){
printf("epoll_wait error,exit\n");
break;
}
for(int i=0;i<nfd;i++){
//使用自定义结构体myevent_s类型指针,接受union data的void* ptr成员
struct myevent_s *ev = (struct myevent_s*)events[i].data.ptr;
if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN)){//读就绪事件
ev->callback(ev->fd,events[i].events,ev->arg);
}
if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT)){//写就绪事件
ev->callback(ev->fd,events[i].events,ev->arg);
}
}
}
return 0;
}
线程池
typedef struct{
void (*function)(void*);//函数指针,回调函数
void *arg;//回调函数的参数
}threadpool_task_t;//各个子线程任务结构体
//描述线程池相关信息
struct threadpool_t{
pthread_mutex_t lock;//用于锁住本结构体
pthread_mutex_t thread_counter;//记录忙状态线程个数的锁 busy_thr_num
pthread_cond_t queue_not_full;//当任务队列满时,添加任务阻塞,等待此条件变量
pthread_cond_t queue_not_empt;//任务队列里不为空时,通知等待任务的线程
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
1. main();
创建线程池。
向线程池中添加任务。 借助回调处理任务。
销毁线程池
2.pthreadpool_create();
创建线程池结构体 指针。
初始化线程池结构体 { N 个成员变量 }
创建 N 个任务线程。
创建 1 个管理者线程。
失败时,销毁开辟的所有空间。(释放)
3.threadpool_thread()
进入子线程回调函数。
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
判断条件变量 --》 wait -------------------170
4.adjust_thread()---管理者线程
循环 10 s 执行一次。
进入管理者线程回调函数
接收参数 void *arg --》 pool 结构体
加锁 --》lock --》 整个结构体锁
获取管理线程池要用的到 变量。 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
5.threadpool_add ()
总功能:
模拟产生任务。 num[20]
设置回调函数, 处理任务。 sleep(1) 代表处理完成。
内部实现:
加锁
初始化 任务队列结构体成员。 回调函数 function, arg
利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。
唤醒阻塞在 条件变量上的线程。
解锁
条件满足,子线程wait被唤醒后处理任务
6.从 3. 中的wait之后继续执行,处理任务。
加锁
获取 任务处理回调函数,及参数
利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。
唤醒阻塞在 条件变量 上的 server。
解锁
加锁
改忙线程数++
解锁
执行处理任务的线程
加锁
改忙线程数——
解锁
线程池扩容和销毁
7. 创建、销毁线程
管理者线程根据 task_num, live_num, busy_num
根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
如果满足 创建条件
pthread_create(); 回调 任务线程函数。 live_num++
如果满足 销毁条件
wait_exit_thr_num = 10;
signal 给 阻塞在条件变量上的线程 发送 假条件满足信号
跳转至 --170 wait阻塞线程会被 假信号 唤醒。判断: wait_exit_thr_num > 0 pthread_exit();
TCP和UDP比较
TCP: 面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。
优点:
稳定。
数据流量稳定,速度稳定,顺序
缺点:
传输速度慢,相率低,开销大。
使用场景:数据的完整型要求较高,不追求效率。
大数据传输、文件传输。
UDP: 无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况
优点:
传输速度块,相率高,开销小。
缺点:
不稳定。
数据流量,速度,顺序。
使用场景:对时效性要求较高场合。稳定性其次。
游戏、视频会议、视频电话。
--- 应用层数据校验协议,弥补udp的不足。
UDP实现C/S模式
recv()/send()只能用于TCP通信,代替read,write
服务器端:
lfd = socket(AF_INET,SOCK_DGRAM);
bind();
listen();-----可有可无
accept()-----舍弃
while(1){
read()----被替换 ----recvfrom()
处理过程
write()---被替换-----sendto()
}
close;
客户端:
cfd = socket(AF_INET,SOCK_DGRAM,0);
connect()----被舍弃
sendto(服务器地址结构,地址结果大小);
recvfrom();
close();
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:传入传出。
返回值: 成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。
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
src_addr:(struct sockaddr *)&addr 传入。 目标地址结构
addrlen:地址结构长度。
返回值:成功写出数据字节数。 失败 -1, errno
//服务端
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#define SERV_PORT 8889
int main(void)
{
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
int sockfd;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Accepting connections ...\n");
while (1) {
clie_addr_len = sizeof(clie_addr);
n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
if (n == -1)
perror("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
if (n == -1)
perror("sendto error");
}
close(sockfd);
return 0;
}
//客户端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#define SERV_PORT 8889
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
int sockfd, n;
char buf[BUFSIZ];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
//bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //其实没用
while (fgets(buf, BUFSIZ, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perror("sendto error");
n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0); //NULL:不关心对端信息
if (n == -1)
perror("recvfrom error");
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
本地套接字
对比网络编程 TCP C/S模型, 注意以下几点:
1. int socket(int domain, int type, int protocol);
参数:
domain:AF_INET --替换为--> AF_UNIX/AF_LOCAL
type: SOCK_STREAM/SOCK_DGRAM 都可以。
3. 地址结构: sockaddr_in --> sockaddr_un
struct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr;
srv_addr.sin_family = AF_INET; --> srv_addr.sun_family = AF_UNIX;
srv_addr.sin_port = htons(8888); --> strcpy(srv_addr.sun_path, "srv.socket")//
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); --> len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");
/*offsetof()函数是求成员变量(直接写名字,不需要.)在结构体的偏移位置*/
bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); --> bind(fd, (struct sockaddr *)&srv_addr, len);
/*为什么不用sizeof(),因为对sockaddr_un而言,除了16位地址结构类型(sun_family)外,剩余的108个字节来存路径名,
我们每次的地址大小都是不一样的,所以需要知道准确值。就只能自己算。*/
4. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket");---->可以理解删除指定文件
5. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:
1) client_addr --> bind()
2) server_addr --> connect();
//服务端
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#define SERV_ADDR "serv.socket"
int main(void)
{
int lfd, cfd, len, size, i;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
lfd = socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* servaddr total len */
unlink(SERV_ADDR); /* 确保bind之前serv.sock文件不存在,bind会创建该文件 */
bind(lfd, (struct sockaddr *)&servaddr, len); /* 参3不能是sizeof(servaddr) */
listen(lfd, 20);
printf("Accept ...\n");
while (1) {
len = sizeof(cliaddr); //AF_UNIX大小+108B
cfd = accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);
len -= offsetof(struct sockaddr_un, sun_path); /* 得到文件名的长度 */
cliaddr.sun_path[len] = '\0'; /* 确保打印时,没有乱码出现 */
printf("client bind filename %s\n", cliaddr.sun_path);
while ((size = read(cfd, buf, sizeof(buf))) > 0) {
for (i = 0; i < size; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, size);
}
close(cfd);
}
close(lfd);
return 0;
}
客户端:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"
int main(void)
{
int cfd, len;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&cliaddr, sizeof(cliaddr));
cliaddr.sun_family = AF_UNIX;
strcpy(cliaddr.sun_path,CLIE_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path); /* 计算客户端地址结构有效长度 */
unlink(CLIE_ADDR);
Bind(cfd, (struct sockaddr *)&cliaddr, len); /* 客户端也需要bind, 不能依赖自动绑定*/
bzero(&servaddr, sizeof(servaddr)); /* 构造server 地址 */
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* 计算服务器端地址结构有效长度 */
Connect(cfd, (struct sockaddr *)&servaddr, len);
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(cfd, buf, strlen(buf));
len = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
close(cfd);
return 0;
}
libevent库
开源。精简。跨平台(Windows、Linux、macos、unix)。专注于网络通信。
特性
:基于“事件”异步通信模型。— 回调。
源码包安装: 参考 README、readme
./configure 检查安装环境 生成 makefile
make 生成 .o 和 可执行文件
sudo make install 将必要的资源cp置系统指定目录。
进入 sample 目录,运行demo验证库安装使用情况。
编译使用库的 .c 时,需要加 -levent 选项。
库名 libevent.so --> /usr/local/lib 查看的到。
Libevent框架:
1. 创建 event_base (乐高底座)
2. 创建 事件evnet
3. 将事件 添加到 base上
4. 循环监听事件满足
5. 释放 event_base
1. 创建 event_base (乐高底座)
struct event_base *event_base_new(void);
struct event_base *base = event_base_new();
2. 创建 事件evnet
常规事件 event --> event_new();
bufferevent --> bufferevent_socket_new();
3. 将事件 添加到 base上
int event_add(struct event *ev, const struct timeval *tv)
4. 循环监听事件满足
int event_base_dispatch(struct event_base *base);
event_base_dispatch(base);
5. 释放 event_base
event_base_free(base);
常规事件event
创建事件event:
struct event *ev;
struct event *event_new(struct event_base *base,evutil_socket_t fd,short what,event_callback_fn cb; void *arg);
注释:
base: event_base_new()返回值。
fd: 绑定到 event 上的文件描述符
what:对应的事件(r、w、e)
EV_READ 一次 读事件
EV_WRTIE 一次 写事件
EV_PERSIST 持续触发。 结合 event_base_dispatch 函数使用,生效。
cb:一旦事件满足监听条件,回调的函数。
typedef void (*event_callback_fn)(evutil_socket_t fd, short, void *)
arg: 回调的函数的参数。
返回值:成功创建的 event
添加事件到 event_base
int event_add(struct event *ev, const struct timeval *tv);
注释:
ev: event_new() 的返回值。
tv:NULL,不会超时。一直等待事件被触发,回调函数会调用。非0,等待期间,检查事件没有触发,时间到,回调函数依旧被触发
摘下事件从event_base-----------了解就行
int event_del(struct event* ev);
注释:
ev: event_new() 的返回值。
销毁事件
int event_free(struct event *ev);
ev: event_new() 的返回值。
主要就3个函数和回调函数就行。
有名管道+libevent库实现
读端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>
// 对操作处理函数
void read_cb(evutil_socket_t fd, short what, void *arg)
{
// 读管道
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
printf("read event: %s \n", what & EV_READ ? "Yes" : "No");
printf("data len = %d, buf = %s\n", len, buf);
sleep(1);
}
// 读管道
int main(int argc, const char* argv[])
{
unlink("myfifo");
//创建有名管道
mkfifo("myfifo", 0664);
// open file
//int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
int fd = open("myfifo", O_RDONLY);
if(fd == -1)
{
perror("open error");
exit(1);
}
// 创建个event_base
struct event_base* base = NULL;
base = event_base_new();
// 创建事件
struct event* ev = NULL;
ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);
// 添加事件
event_add(ev, NULL);
// 事件循环
event_base_dispatch(base); // while(1) { epoll();}
// 释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
写端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>
// 对操作处理函数
void write_cb(evutil_socket_t fd, short what, void *arg)
{
// write管道
char buf[1024] = {0};
static int num = 0;
sprintf(buf, "hello,world-%d\n", num++);
write(fd, buf, strlen(buf)+1);
sleep(1);
}
// 写管道
int main(int argc, const char* argv[])
{
// open file
//int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
int fd = open("myfifo", O_WRONLY);
if(fd == -1)
{
perror("open error");
exit(1);
}
// 写管道
struct event_base* base = NULL;
base = event_base_new();
// 创建事件
struct event* ev = NULL;
// 检测的写缓冲区是否有空间写
//ev = event_new(base, fd, EV_WRITE , write_cb, NULL);
ev = event_new(base, fd, EV_WRITE | EV_PERSIST, write_cb, NULL);
// 添加事件
event_add(ev, NULL);
// 事件循环
event_base_dispatch(base);
// 释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
未决和非未决:
非未决: 没有资格被处理
未决: 有资格被处理,但尚未被处理
event_new() --> event 处于 非未决 --> event_add() --> event处于未决 --> 满足dispatch() && 监听事件被触发 --> event激活态
--> 执行回调函数 --> 被处理(非未决)--> event_add && EV_PERSIST --> 未决 --> event_del --> 非未决
带缓冲区的事件 bufferevent
#include <event2/bufferevent.h>
原理:read/write 两个缓冲区-->借助队列.读走就没有,先进先出
读:有数据-->读回调函数被调用-->使用bufferevent_read()-->读数据
写:使用bufferevent_write()-->向写缓冲区写数据-->该缓存区由数据自动写出-->写完,回调函数被调用(通知写数据完成,鸡肋)
bufferevent事件对象创建、销毁
创建、销毁bufferevent:
struct bufferevent *ev;
struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);
注释:
base: event_base
fd: 封装到bufferevent内的 fd
options:BEV_OPT_CLOSE_ON_FREE
/*释放bufferevent时关闭底层传输端口。这将关闭底层套接字,释放底层bufferevent等。*/
返回: 成功创建的 bufferevent事件对象。
void bufferevent_socket_free(struct bufferevent *ev);
给bufferevent事件对象设置回调
给bufferevent设置回调:
对比event: event_new(...,fd,callback); -->event_add() -- 挂到 event_base 上。
bufferevent_socket_new(...,fd) --> bufferevent_setcb(callback)
void bufferevent_setcb(struct bufferevent * bufev,
bufferevent_data_cb readcb,
bufferevent_data_cb writecb,
bufferevent_event_cb eventcb,
void *cbarg );
注释:
bufev: bufferevent_socket_new() 返回值
readcb: 设置 bufferevent 读缓冲,对应回调 read_cb{ bufferevent_read() 读数据 }
writecb: 设置 bufferevent 写缓冲,对应回调 write_cb { } -- 给调用者,发送写成功通知。 可以 NULL
eventcb: 设置 事件回调。 也可传NULL
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void*ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);
events: BEV_EVENT_CONNECTED
cbarg: 上述回调函数使用的 参数。
read 回调函数类型:
void read_cb(struct bufferevent *bev, void *cbarg )
{
.....
bufferevent_read(); --- read();
}
size_t bufferevent_read(struct bufferevent *bev, void *buf, size_t bufsize);
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
启动、关闭 bufferevent的 缓冲区:
void bufferevent_enable(struct bufferevent *bufev, short events); 启动
events: EV_READ、EV_WRITE、EV_READ|EV_WRITE
默认、write 缓冲是 enable、read 缓冲是 disable
bufferevent_enable(evev, EV_READ); -- 开启读缓冲(必须,不然无法读)。
void bufferevent_disable(struct bufferevent* bufev,short events);禁用
short bufferevent_get_enabled(struct bufferevent* bufev); 获取缓存区的禁用状态,借助&得到
基于bufferevent的网络通信
服务端:
创建监听服务器:
struct evconnlistener* listner
struct evconnlistener* evconnlistener_new_bind (
struct event_base *base,
evconnlistener_cb cb,
void *ptr,
unsigned flags,
int backlog,
const struct sockaddr *sa,
int socklen);
注释:
base: event_base
cb: 回调函数。 一旦被回调,说明在其内部应该与客户端完成, 数据读写操作,进行通信。
typedef void(*evconnlistener_cb)(struct evconnlistener* listener,evutil_socket_t sock,struct sockaddr* addr,int len,void* ptr);
listen:evconnlistener_new_bind()的返回值
sock:用于通信的文件描述符
addr:客户端的ip+端口
len:addr的长度
ptr:外部ptr传递的参数
注意:该回调函数,不由我们调用,是框架自动调用。因此,只需要知晓参数含义,在函数内部会用就行。
ptr: 回调函数的参数
flags: LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE
backlog: listen() 2参。 -1 表最大值
sa:服务器自己的地址结构体
socklen:服务器自己的地址结构体大小。
返回值:成功创建的监听器。
释放监听服务器
void evconnlistener_free(struct evconnlistener *lev);
客户端:
int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);
bev: bufferevent 事件对象(封装了fd)
address、len:等同于 connect() 参2,3
libevent实现TCP服务器流程
1. 创建event_base
2. 使用 evconnlistener_new_bind 创建监听服务器, 设置其回调函数,当有客户端成功连接时,这个回调函数会被调用。
3. 封装 listner_cb() 在函数内部。完成与客户端通信处理接受连接后得操作。
4. 回调函数被调用,说明有一个新客户端连接上来。会得到一个新fd,用于跟客户端通信(读写)
5. 创建bufferevent事件对象,将fd封装到里面。bufferevent_socket_new();
6. 使用bufferevent_setcb() 函数给 bufferevent的 read、write、event 设置回调函数。
7. 设置bufferevent读缓冲、写缓冲的 使能状态 enable、disable
8. 接受、发送数据 bufferevent_read()/bufferevent_write()
9. 启动循环监听 event_base_dispath()
10. 释放连接。
libevent实现TCP客户端流程
1. 创建 event_base;
2. 使用bufferevent_socket_new()创建一个跟服务器通信的bufferevent事件对象;
3. 使用bufferevent_socket_connect()连接服务器;
4. 使用bufferevent_setcb()给事件对象的read、write、event设置回调;
5. 设置事件对象的读写缓冲区使能状态enable/disable;
6. 接受、发送数据bufferevent_read()/bufferevent_write();
7. 释放资源;
服务端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
// 读缓冲区回调
void read_cb(struct bufferevent *bev, void *arg)
{
char buf[1024] = {0};
bufferevent_read(bev, buf, sizeof(buf));
printf("client say: %s\n", buf);
char *p = "我是服务器, 已经成功收到你发送的数据!";
// 发数据给客户端
bufferevent_write(bev, p, strlen(p)+1);
sleep(1);
}
// 写缓冲区回调
void write_cb(struct bufferevent *bev, void *arg)
{
printf("I'm 服务器, 成功写数据给客户端,写缓冲区回调函数被回调...\n");
}
// 事件
void event_cb(struct bufferevent *bev, short events, void *arg)
{
if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
else if(events & BEV_EVENT_ERROR)
{
printf("some other error\n");
}
bufferevent_free(bev);
printf("buffevent 资源已经被释放...\n");
}
void cb_listener(struct evconnlistener *listener,evutil_socket_t fd,
struct sockaddr *addr,int len, void *ptr)
{
printf("connect new client\n");
struct event_base* base = (struct event_base*)ptr;
// 通信操作
// 添加新事件
struct bufferevent *bev;
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// 给bufferevent缓冲区设置回调
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
bufferevent_enable(bev, EV_READ);
}
int main(int argc, const char* argv[])
{
// init server
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(8889);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
//创建event_base
struct event_base* base;
base = event_base_new();
// 创建套接字
// 绑定
// 接收连接请求
struct evconnlistener* listener;
listener = evconnlistener_new_bind(base, cb_listener, base,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
36, (struct sockaddr*)&serv, sizeof(serv));
event_base_dispatch(base);
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
//客户端
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/bufferevent.h>
#include <event2/event.h>
#include <arpa/inet.h>
void read_cb(struct bufferevent *bev, void *arg)
{
char buf[1024] = {0};
bufferevent_read(bev, buf, sizeof(buf));
printf("fwq say:%s\n", buf);
bufferevent_write(bev, buf, strlen(buf)+1);
sleep(1);
}
void write_cb(struct bufferevent *bev, void *arg)
{
printf("----------我是客户端的写回调函数,没卵用\n");
}
void event_cb(struct bufferevent *bev, short events, void *arg)
{
if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
else if(events & BEV_EVENT_ERROR)
{
printf("some other error\n");
}
else if(events & BEV_EVENT_CONNECTED)
{
printf("已经连接服务器...\\(^o^)/...\n");
return;
}
// 释放资源
bufferevent_free(bev);
}
// 客户端与用户交互,从终端读取数据写给服务器
void read_terminal(evutil_socket_t fd, short what, void *arg)
{
// 读数据
char buf[1024] = {0};
int len = read(fd, buf, sizeof(buf));
struct bufferevent* bev = (struct bufferevent*)arg;
// 发送数据
bufferevent_write(bev, buf, len+1);
}
int main(int argc, const char* argv[])
{
struct event_base* base = NULL;
base = event_base_new();
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 通信的fd放到bufferevent中
struct bufferevent* bev = NULL;
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// init server info
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(9876);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
// 连接服务器
bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));
// 设置回调
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
// 设置读回调生效
bufferevent_enable(bev, EV_READ);
// 创建事件 ---与终端交互
struct event* ev = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST,
read_terminal, bev);
// 添加事件
event_add(ev, NULL);
event_base_dispatch(base);
event_free(ev);
event_base_free(base);
return 0;
}