一、IO的定义
IO 即“Input”和“Output”的组合,即输入/输出,IO用来处理设备之间的数据传输。socket/fd也是一种IO。
二、socket的定义
socket 的译意是“插座”,在计算机通信领域,socket 也被翻译为“套接字”,它是计算机之间进行通信的一种方式。通过 socket ,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
三、一对一服务器设计
第一步:创建socket。
函数原型
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol);
这个函数建立一个协议族、协议类型、协议编号的socket文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
domain参数值含义:
名称 | 含义 |
---|---|
PF_UNIX,PF_LOCAL | 本地通信 |
AF_INET,PF_INET | IPv4协议 |
PF_INET6 | IPv6协议 |
PF_NETLINK | 内核用户界面设备 |
PF_PACKET | 底层包访问 |
type参数值含义:
名称 | 含义 |
---|---|
SOCK_STREAM | TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | UDP连接 |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
:SOCK_PACKET | 专用类型 |
SOCK_RDM | 提供可靠的数据报文,不保证数据有序 |
SOCK_RAW | 提供原始网络协议访问 |
protocol参数含义:
通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;如果协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
第二步:设置参数
通过struct sockaddr_in结构体指定协议族,指定绑定地址,指定监控的端口号。
使用的成员:sin_family、sin_addr.s_addr、sin_port
第三步:绑定--> bind
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第2个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。
第3个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败
第四步:监听--> listen
函数原型:
#include<sys/socket.h>
int listen(int sockfd, int backlog);
参数说明:
第1个参数sockfd是用socket()函数创建的文件描述符。
第二个参数规定了内核应该为相应套接字排队的最大连接个数。
第五步:接收连接--> accept
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
参数说明:
sockefd:套接字描述符,该套接字在listen()后监听连接。
addr:(可选)指针。指向一个缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
addrlen:(可选)指针。输入参数,配合addr一起使用,指向存有addr地址长度的整形数。
第六步:接收数据--> recv
函数原型:
#include<sys/types.h>
#include<sys/socket.h>
int recv( int fd, char *buf, int len, int flags);
参数说明:
第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
第七步:发送数据-->send
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
sockfd:向套接字中发送数据
buf:要发送的数据的首地址
len:要发送的数据的字节
int flags:设置为MSG_DONTWAITMSG 时 表示非阻塞,设置为0时 功能和write一样
返回值:成功返回实际发送的字节数,失败返回 -1
完整示例:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#define BUFF_LENGTH 128
int main(void)
{
int listen_fd=socket(AF_INET,SOCK_STRAM,0);
if(lisenfd==-1)
return -1;
printf("lisenfd: %d\n",lisenfd);
struct sockaddr_in servaddr;
servaddr.sin_family=AF_INET;//指定协议族,INET是IPv4,INET6是IPv6
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//指定地址
servaddr.sin_port=htons(9999);//将整型变量从主机字节顺序转变成网络字节顺序
//bind(listenfd,&servaddr,sizeof(servaddr));
if(-1==bind(lisenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)))
{
return -2;
}
listen(listenfd,10);
struct sockaddr_in client;
socklen_t len=sizeof(client);
int clientfd=accept(lisenfd,(struct sockaddr*)&client,&len);
printf("client: %d\n",clientfd);
while(1){
unsigned char buffer[BUFF_LENGTH] = {
0 };
int ret = recv(clientfd,buffer,BUFF_LENGTH,0);
printf("buffer: %s,ret=%d\n",buffer,ret);
ret=send(clientfd,buffer,ret,0);
printf("send buffer: %s,ret=%d\n",buffer,ret);
}
return 0;
}
四、设置非阻塞
默认的连接是阻塞方式的,可以使用fcntl函数进行设置非阻塞模式。
函数原型:
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
// 返回值:成功依赖cmd的值,失败返回-1;
cmd参数说明:
参数 | 含义 |
---|---|
F_GETFL | 获取文件状态标志 |
F_SETFL | 设置文件状态标志 |
F_GETFD | 获取文件描述符标志 |
F_SETFD | 设置文件描述符标志 |
F_GETLK | 获取文件锁 |
F_SETLK | 设置文件锁 |
F_DUPFD | 复制文件描述符 |
F_GETOWN | 取当前接受SIGIO和SIGURG信号的进程ID和进程组ID.正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程中ID |
F_SETOWN | 设置当前接受SIGIO和SIGURG信号的进程ID和进程组ID. |
状态标志:
标志 | 含义 |
---|---|
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 为读、写打开 |
O_APPEND | 每次写时追加 |
O_NONBLOCK | 非阻塞模式 |
O_SYNC | 等待写完成(数据和属性) |
O_DSYNC | 等待写完成(数据) |
O_RSYNC | 同步读、写 |
O_FSYNC | 等待写完成(进FreeBSD和Mac OS X) |
O_ASYNC | 异步I/O(进FreeBSD和Mac OS X) |
注意: 非阻塞要在accpt函数之前设置才能生效。
使用示例:
int flag=fcntl(listenfd,F_GETFL,0);
flg|=O_NONBLOCK
fcntl(listenfd,F_SETFL,0);
五、多对一服务器设计
5.1、多线程方案
使用多线程方案,来一个连接请求则创建一个线程。
pthread_create函数原型:
#include <pthread.h>
int pthread_create(
pthread_t *restrict tidp, //新创建的线程ID指向的内存单元。
const pthread_attr_t *restrict attr, //线程属性
void *(*start_rtn)(void *), //线程函数的地址
void *restrict arg //线程函数所需的参数
);
完整示例:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#define BUFFER_LENGTH 128
// thread --> fd
void *routine(void *arg)
{
int clientfd = *(int *)arg;
while (1) {
unsigned char buffer[BUFFER_LENGTH] = {
0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0)
{
close(clientfd);
break;
}
printf("buffer : %s, ret: %d\n", buffer, ret);
ret = send(clientfd, buffer, ret, 0);
}
}
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //
if (listenfd == -1) return -1;
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
#if 0 // nonblock
int flag = fcntl(listenfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(listenfd, F_SETFL, flag);
#endif
listen(listenfd, 10);
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
pthread_t threadid;
pthread_create(&threadid, NULL, routine, &clientfd);
}
return 0;
}
5.2、io多路复用——select
什么是IO多路复用? 通俗的讲就是一个线程,通过记录IO流的状态来管理多个IO。解决创建多个进程处理IO流导致CPU占用率高的问题。
select是io多路复用的一种方式,其他的还有poll、epoll等。
函数原型:
#include <sys/types.h>
#include <unistd.h>
int select(int maxfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
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);
select函数共有5个参数,其中参数和返回值:
maxfds:监视对象文件描述符数量。
readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值。
writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值。
exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值。
timeout:调用select后,为防止陷入无限阻塞状态,传递超时信息。
返回值:错误返回-1,超时返回0。当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
完整示例:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //
if (listenfd == -1) return -1;
// listenfd
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
#if 0 // nonblock
int flag = fcntl(listenfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(listenfd, F_SETFL, flag);
#endif
listen(listenfd, 10);
fd_set rfds, wfds, rset, wset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int maxfd = listenfd;
unsigned char buffer[BUFFER_LENGTH] = {
0}; // 0
int ret = 0;
// int fd,
while (1) {
rset = rfds;
wset = wfds;
int nready = select(maxfd+1, &rset, &wset, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = listenfd+1; i <= maxfd;i ++) {
if (FD_ISSET(i, &rset)) {
//
ret = recv(i, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
close(i);
FD_CLR(i, &rfds);
} else if (ret > 0) {
printf("buffer : %s, ret: %d\n", buffer, ret);
FD_SET(i, &wfds);
}
} else if (FD_ISSET(i, &wset)) {
ret = send(i, buffer, ret, 0); //
FD_CLR(i, &wfds); //
FD_SET(i, &rfds);
}
}
}
return 0;
}
步骤:
1、定义io管理状态变量:fd_set rfds,wfds;
2、初始化变量:FD_ZERO();
3、设置io流状态,最初只有监听的fd,将其设置:FD_SET(listenfd,rfds);
4、在循环中select
5、FD_ISSET()判断端口是否有连接
6、FD_ISSET()判断可读、可写状态
总结
本文通过对Linux网络IO和select的详细讨论,帮助读者深入理解了这些关键概念,并展示了select函数在构建高效网络应用中的重要性和灵活性。对于想要提升网络编程技能的开发者来说,这些知识将会是宝贵的参考和实践指南。
网络IO的重要性:理解网络IO是构建高效网络应用的基础。通过有效管理数据的输入和输出,可以实现更好的性能和可伸缩性。
Linux中的网络IO模型:介绍了阻塞IO、非阻塞IO、多路复用IO和异步IO等不同的网络IO模型。特别地,我们重点讨论了多路复用IO模型中的select函数。
select函数的作用:select函数是一种常用的多路复用机制,它可以同时监视多个文件描述符的状态变化,并通知应用程序哪些描述符可以进行读写操作。
使用select函数的优势:通过使用select函数,可以在一个线程内管理多个连接,减少了线程创建和销毁的开销,提升了系统的性能和资源利用率。
select函数的工作原理:详细解释了select函数的工作原理,包括文件描述符集合的准备、调用select函数并处理返回结果的流程。
select函数的限制:虽然select函数具有一定的优点,但也存在一些限制,如最大文件描述符数量的限制,每次调用都需要遍历整个描述符集合等。
select函数的应用示例:通过一个实际的案例,演示了如何使用select函数实现多个TCP连接的并发处理,展示了其在网络编程中的具体应用。