网络编程:网络IO与select
1. 网络IO是什么?
IO的含义是输入输出(input/output)
网络IO本质上也是IO的一种,涉及到两个系统的对象
- 用户空间调用IO的进程或者线程
- 内核空间的内核系统
2. socket是什么?
socket套接字是一种通信机制,提供了TCP/IP协议的抽象,对外提供了一套接口,通过这个接口可以统一、方便的使用TCP/IP协议的功能
3. FD是什么?
FD(File descriptor),文件描述符是一个非负整数,本质上是一个索引值
FD的取值范围:0~OPEN_MAX - 1
- 在POSIX的语义中0,1,2这三个fd值被分别赋予为标准输入(STDIN_FILENO),标准输出(STDOUT_FILENO),标准错误(STDERR_FILENO)
- 最大打开文件数通过ulimit命令查看,例如:ulimit -n
当打开一个文件时,内核向进程返回一个文件描述符(open系统调用得到),后续read、write这个文件时,只需要用这个文件描述符来标识该文件,将其作为参数传入read、write就能读写文件
4. Linux网络连接(单连接)
下面是最基础的TCP端口监听代码,默认创建socket的listenfd是阻塞的,如果需要变成非阻塞的,需要使用fcntl这个函数去对listenfd做或操作(位运算)
下面的代码现在只能与一个客户端建立连接(只能连接一次),可以多客户端连接,但是只会接收第一个连接进来的客户端的信息
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>
#define BUFFER_LENGTH 128
int main(){
// 1. 使用socket套接字创建fd, fd默认是阻塞的
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
printf("create listend fd failed");
return -1;
}
// 2. listenfd绑定一个固定的地址
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
printf("bind failed");
return -2;
}
// 修改fd的性质, 改为非阻塞
// int flag = fcntl(listenfd, F_GETFL, 0);
// flag |= O_NONBLOCK; // 如果直接set可能会把原来已有的属性给覆盖掉,所以需要或
// fcntl(listenfd, F_SETFL, flag);
// 3. 新建一个监听队列
// Prepare to accept connections on socket FD.
// N connection requests will be queued before further requests are refused.
listen(listenfd, 10);
// 4. 新建一个连接客户端的fd
/* Structure describing an Internet socket address. */
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 此时listenfd是一个阻塞的, 因为新建socket的时候默认就是阻塞的
// Await a connection on socket FD.
// When a connection arrives, open a new socket to communicate with it
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
while(1){
// 4.1 新建一个接收缓冲区
unsigned char buffer[BUFFER_LENGTH] = {
0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
printf("buffer: %s, ret: %d\n", buffer, ret);
// Send N bytes of BUF to socket FD. Returns the number sent or -1.
ret = send(clientfd, buffer, ret, 0);
}
5. Linux网络连接(多线程)
为了解决上面的只能和第一个客户端通信的问题,使用多线程的方式改进
使用多线程处理多客户端连接,会引出一个问题,如果1万个客户端连接,就得新建1万个线程,这个开销是有点大的
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>
#define BUFFER_LENGTH 128
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(){
// 1. 使用socket套接字创建fd, fd默认是阻塞的
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
printf("create listend fd failed");
return -1;
}
// 2. listenfd绑定一个固定的地址
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
printf("bind failed");
return -2;
}
// 3. 新建一个监听队列
listen(listenfd, 10);
while(1){
// 4. 新建连接客户端的fd
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
// 创建线程去处理客户端fd发来的请求
pthread_t threadid;
pthread_create(&threadid, NULL, routine, &clientfd);
}
6. IO多路复用
IO多路复用是网络编程中常提到的select/epoll,也称为事件驱动IO(event driven IO),作用是检测IO是否有事件
- 优点:单个进程可以同时处理多个网络连接的IO(轮询)
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>
#define BUFFER_LENGTH 128
int main(){
// 1. 使用socket套接字创建fd, fd默认是阻塞的
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
printf("create listend fd failed");
return -1;
}
// 2. listenfd绑定一个固定的地址
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
printf("bind failed");
return -2;
}
listen(listenfd, 10);
// 3. 定义读事件和写事件
fd_set rfds, wfds, rset, wset;
// 3.1 清空readfd集合中所有bit, 置为0
FD_ZERO(&rfds);
// 3.2 将listenfd加入到fd读集合中
FD_SET(listenfd, &rfds);
// 一定要比实际在用的fd大
int maxfd = listenfd;
int ret = 0;
unsigned char buffer[BUFFER_LENGTH] = {
0};
while(1) {
// 3.3 把判断位 赋值给 修改位
rset = rfds;
wset = wfds;
// 可读集合 可写集合
// select其实是一个for循环
// 返回实时的有多少个
int nready = select(maxfd + 1, &rset, &wset, NULL, NULL);
// 判断fd在不在rset中, rset是一个二进制位, 就看有没有这个位
if(FD_ISSET(listenfd, &rset)){
printf("listenfd --> \n");
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 新增加的fd, 也加到可读的fd集合中
int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
FD_SET(clientfd, &rfds);
if(clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
// 4. 由于fd是依次增加的, 从listenfd开始遍历, 一直到最大的fd
for(i = listenfd + 1; i <= maxfd; i++){
// 4.1 判断当前fd是不是在读集合
if(FD_ISSET(i, &rset)){
// 4.2 读取这个fd的缓冲区
ret = recv(i, buffer, BUFFER_LENGTH, 0);
// 4.3 如果recv的返回是0,表面客户端已经关闭了
if(ret == 0){
close(i);
// 4.4 再把当前fd的事件clear掉
FD_CLR(i, &rfds);
}else if(ret > 0){
// 4.5 从缓冲区中读到了数据
printf("buffer: %s, ret: %d\n", buffer, ret);
// 4.6 将当前fd加入到写集合中
FD_SET(i, &wfds);
// FD_CLR(i, &rfds); 这行要注释吗?
}
}
// 5. 如果上面的fd读取到了内容
if(FD_ISSET(i, &wset)){
// 5.1 发送数据到客户端
ret = send(i, buffer, ret, 0);
FD_CLR(i, &wfds);
FD_SET(i, &rfds);
}
}
}
}
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:
Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习