一、TCP客户服务器编程模型
客户端
调用socket函数创建套接字。
int socket(int domain, int type, int protocol);
头文件:#include<sys/socket.h>
参数说明:
socket:指出协议族(如AF_INET指定IPV4)
type:指定协议类型,SOCK_STREAM指定字节流协议(如TCP),SOCK_DGRAM指定数据报协议(如UDP)。
protocol:指定具体协议,通常传0
返回值:成功返回非负的 套接字,失败返回-1
调用connect连接服务器端。
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen );
头文件:#include<sys/types.h>
#include<sys/socket.h>
参数说明:
sockfd: socket函数返回的套接字
serv_addr: 服务器地址
IPV4地址族
#include <netinet/in.h>
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in {
uint16_t sin_family;//地址族,如AF_INET, 主机字节序。
uint16_t sin_port;//端口号,16位值,网络字节序。
struct in_addr sin_addr;//IPv4地址,一个32位整数,网络字节序。
char sin_zero[8];
};
addrlen: sockaddr结构体的长度
返回值:成功返回0,失败返回-1并设置errno
调用I/O函数(read/write)与服务器端通讯。
int read (int sockfd, void* buf, size_t nbytes);
int write(int sockfd, void*buf, size_t nbytes);
头文件:#icnlude<unistd.h>
参数说明:
sockfd:已连接套接字
*buf:保存数据的缓冲区
nbytes:期望写入(或写出)的字节数
返回值:返回读或者写的字节数,失败返回-1并设置errno
调用close关闭套接字。
int close(int sockfd);
头文件: #include<unistd.h>
参数说明:
sockfd:socket函数返回的套接字
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
void hangle(int sig)
{
printf("recv sig: %d\n", sig);
}
int main()
{
signal(SIGPIPE, hangle);
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8000);
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
ret = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr_in)) ;
if(ret < 0)
{
perror("connect");
return -1;
}
char buff[1024];
while(1)
{
printf("请输入数据:");
scanf("%s", buff);
ret = write(sockfd, buff, strlen(buff));
if(ret<0)
{
perror("write");
return -1;
}
}
close(sockfd);
return 0;
}
服务器
调用socket函数创建套接字。
同客户端
调用bind指定本地地址和端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
头文件:#include<sys/types.h>
#include<sys/soccket.h>
参数说明:
sockfd:socket函数返回的套接字
*addr:本地端口和地址
addrlen:addr的字节数
返回值:成功返回0不成功返回-1,并把错误码存放在全局变量errno中
一台主机可以有多个网络接口和多个IP地址,如果我们只关心某个地址的连接请求,我们可以指定一个具体的本地IP地址
#define INADDR_ANY (uint32_t)0x00000000
这个地址可以响应所有接口上的连接请求
调用listen启动监听。
int listen(int sockfd, int backlog);
头文件:#include<sys/socket.h>
参数说明:
sockfd :成功调用socket函数返回的套接字,并已经成功调用bind。
backlog: 告诉套接字在忙于处理上一个请求时还可以接受多少个进入的请求,换句话说,这决定了挂起连接的队列的大小.
返回值:成功返回0,失败返回-1并将错误码存放与全局变量errno中。
调用accept从已连接列队中提取客户连接。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
头文件:#include<sys/socket.h>
参数说明:
sockfd:成功调用socket()返回的套接字
addr:接收连接客户端的地址
addrlen:addr的长度
返回值:成功返回客户端套接字,失败返回-1并将错误码存放与全局变量errno中。
一般与connect对称使用
调用I/O函数(read/write)与客户端通讯。
同客户端
调用close关闭连接。
同客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#define SERADDR "127.0.0.1"
#define SERPORT 8000
//成功返回监听套接字, 失败返回-1
int sockfd_init()
{
int sockfd, ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//设置端口复用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(SERPORT);
inet_pton(AF_INET, SERADDR, &seraddr.sin_addr.s_addr);
ret = bind(sockfd, (struct sockaddr*)&seraddr, sizeof(struct sockaddr_in));
if(ret<0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 20);
if(ret < 0)
{
perror("listen");
return -1;
}
return sockfd;
}
int main()
{
int sockfd, ret;
int cfd, efd, count;
sockfd = sockfd_init();
if(sockfd<0)
{
return -1;
}
efd = epoll_create(10);
if(efd < 0)
{
perror("epoll_create");
return -1;
}
struct epoll_event ev, evs[10];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev);
char buff[1024];
while(1)
{
printf("wait...\n");
count = epoll_wait(efd, evs, 10, -1);
printf("wait over...\n");
if(count<0)
{
perror("epoll_wait");
break;
}
for(int i=0; i<count; i++)
{
int temp = evs[i].data.fd;
if(temp == sockfd) //说明有客户端请求连接
{
//1、接收客户端
printf("accept...\n");
cfd = accept(sockfd, NULL, NULL);
printf("accept over...\n");
if(cfd<0)
{
perror("accept");
continue;
}
//2、cfd加入efd集合中
ev.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
}
else //说明已经连接的客户端发来数据
{
printf("Read...\n");
ret = read(temp, buff, 1024 );
printf("Read over...\n");
if(ret< 0)
{
//1、打印错误信息
perror("read");
//2、关闭文件描述符
close(temp);
//3、从集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
else if(0 == ret)
{
//1、打印错误信息
printf("tcp broken...\n");
//2、关闭文件描述符
close(temp);
//3、从集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
}
return 0;
}
三、epoll、select
在 Linux 中,epoll 是一种高效的多路复用机制,比 select 更快。epoll 是基于事件驱动的机制,可以同时监视大量的文件描述符,并且支持水平触发和边缘触发两种工作方式。相比于 select,epoll 有以下优点:
无限制的文件描述符:epoll 可以监视数十万个文件描述符,而 select 和 poll 机制的可监视文件描述符数量通常受到系统限制。
更高效的事件通知:epoll 只会通知发生变化的文件描述符,而 select 会通知所有监视的文件描述符,需要应用程序自行处理哪些文件描述符发生了变化。
更快的速度:epoll 在大量文件描述符的情况下,比 select 更快,因为 select 在每次调用时都需要遍历所有的监视对象,而 epoll 可以只通知有变化的对象。
更少的内存使用:epoll 使用的内存比 select 少,因为 epoll 会维护一个事件表,而 select 会维护一个监视对象列表。
epoll
头文件:#include<sys/epoll.h>
根据文件描述符的个数创建空间
int epoll_create(int size);
参数:size:设置监听文件描述符的个数
返回值:成功返回与集合空间关联的文件描述符,失败返回-1;
控制集合空间的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
epfd:和集合关联的文件描述符
op:操作命令
EPOLL_CTL_ADD:往集合控件添加文件描述符
EPOLL_CTL_DEL:从集合空间删除文件描述符的命令
fd:操作的文件描述符
event:如果是删除操作,可直接忽略,默认传NULL;
如果是添加操作,通过参数告诉内核监听某个文件描述符的某个事件
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /*设置监听的事件: EPOLLIN(读事件) */
epoll_data_t data; /* data.fd :设置监听文件描述符*/
};
监听集合中文件描述符
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
参数说明:
epfd:和集合关联的文件描述符
events:接受准备文件描述符信息数组的起始地址
maxevents:数组的最大元素个数
timeout:设置超时时间(以毫秒为单位)
>0:设置超时时间
=0:该函数是一个非阻塞函数
-1:永久阻塞
返回值:>0:准备好的文件描述符个数
=0:超时解除阻塞
-1:出错
select
头文件:#include<sys/select.h>或#include<sys/time
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds: 监听最大文件描述符+1
readfds: 监听读集合事件的起始地址
writefds: 监听写集合事件的起始地址,
exceprfds: 监听异常集合事件的起始地址,
timeout: 设置超时时间
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
>0: 有响应文件描述符的个数
=0:超时时间到
-1: 出错
void FD_CLR(int fd, fd_set *set); //将fd从set集合中删除
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中, 在返回真,否则返回假
void FD_SET(int fd, fd_set *set); // 将fd加入set集合中
void FD_ZERO(fd_set *set); //清空set集合
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int maxfd, ret;
//1、创建集合
fd_set set, rset;
//2、将监听文件描述符加入set集合
FD_SET(0, &set);
//3、设置最大值
maxfd = 0;
char buff[1024];
struct timeval tv;
tv.tv_sec = 2;
tv.tv_usec = 0;
while(1)
{
rset = set;
//4、调用select函数监听集合
printf("select...\n");
ret = select(maxfd+1, &rset, NULL, NULL, &tv);
printf("select over...\n");
if(ret<0)
{
perror("select");
break;
}
if(0 == ret)
{
printf("time out...\n");
tv.tv_sec = 2;
tv.tv_usec = 0;
continue;
}
//5、采用暴力的方式找出有响应的文件描述符
for(int i=0; i<=maxfd; i++)
{
if(FD_ISSET(i, &rset))
{
printf("read...\n");
ret = read(i, buff, 1024);
printf("read over...\n");
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
}
return 0;
}