TCP网络编程学习

单进程处理单个客户端

额外知识补充:
端口号:端口号用来识别同一台计算机中进行通信的不同应用程序。
​端口号 是一个 ​16 位整数,范围从 065535。
常见端口号:http:80 ssh:22
知名端口:0~1023 预留给系统或知名服务
注册端口	 1024~49151	分配给用户或企业注册的应用
动态/私有端口 49152~65535	客户端临时使用(操作系统自动分配)

INADDR_ANY 内核会自动将套接字绑定到所有可用的网络接口(计算机上的物理和虚拟网卡,通过ipconfig可以查看)。
INADDR_LOOPBACK 仅监听本地回环接口(127.0.0.1)

端口复用:端口复用(Port Reuse)是指在同一台机器上,允许多个套接字绑
定到相同的 IP 地址和端口号。(在服务器监听文件描述符绑定端口之前设置)

strlen不计算末尾的\0.
read读到数据之后不会在尾部添加\0,可能需要接收端程序主动添加\0

特别注意:addrlen一定要被初始化,否则客户端第一次接入读取的ip和端口号是错的
//第四步:接收客户端连接
sockaddr_in sockaddrClient;
socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);
//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
read/write:所有文件描述符(文件、管道、套接字等),默认阻塞
recv/send: 仅用于套接字文件描述符,支持更多控制选项
所以套接字的话以后都用send和recv

基础版服务端代码(详细注释)

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
const int PORT = 8080;
int main() {
    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
    while (1) {
        int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
        if (cfd == -1) {
            if (errno == EINTR) {
                std::cout << "EINTR";
                continue; 
            }
            perror("accept");
            exit(-1);
        }
        //尝试打印客户端信息,端口号+ip地址
        std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;
        //读取客户端信息
        char buf[1024];
        while (1) { 
            int readLen = read(cfd, buf, sizeof(buf));
            if (readLen > 0) {
                //打印读到的信息
                buf[readLen] = '\0';
                printf("%s",buf);
            } else if(readLen == 0) {
                //客户端已关闭连接
                close(cfd);
                break;//退出与客户端的信息交互
            } else {
                perror("read");
                close(cfd);
                break;
            }
            //读到数据后进行回传
            const char* message = "this is server:";
            int writeLen = write(cfd, message,strlen(message));
            if (writeLen < 0) {
                perror("write");
                close(cfd);
                break;
            }
            writeLen = write(cfd, buf,readLen);
            if (writeLen < 0) {
                perror("write");
                close(cfd);
                break;
            }
        }
    }
    close(lfd);
    return 0;
}

基础版客户端代码(详细注释)

//构建客户端
//TCP构建客户端
/*
1.socket,得到监听fd
2.connect建立连接
3.send/write
4.recv/read
7.close
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
const int PORT = 8080;
int main() {
    //第一步:创建套接字用户通信
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd < 0) {
        perror("socket");
        exit(-1);
    }
    //连接服务器:需要指定连接的服务器ip和端口
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//以这个ip地址访问服务器,其实就是本机的虚拟网卡IP
    int isConnectOK = connect(cfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isConnectOK == -1) {
        perror("connect");
        exit(-1);
    }
    char buf[1024];
    while (1) {
        if (fgets(buf,sizeof(buf),stdin) == NULL) {
            perror("fgets");
            break;
        } 
        //fgets读完之后buf的末尾会有一个\n需要特别注意一下
        int writeLen = write(cfd, buf,strlen(buf));
        if (writeLen < 0) {
            perror("write");
            break;
        }
        int readLen = read(cfd, buf, sizeof(buf));
        if (readLen > 0) {
            //打印读到的信息
            buf[readLen] = '\0';
            printf("%s",buf);
        } else if(readLen == 0) {
            //客户端已关闭连接
            close(cfd);
            break;//退出与客户端的信息交互
        } else {
            perror("read");
            break;
        }
    }
    return 0;
}

多进程服务器代码

涉及一个问题,如何处理在一个进程结束后进行wait(0),因为主进程服务器一直在运行,无法运行wait,因为wait会造成阻塞,主进程服务器需要一直accept接收客户端连接。
使用进程间通信机制:信号
特别注意两个点:
1.wait(&status)阻塞等待任意进程
2.waitpid(pid,&status,options)可以等待回收指定的子进程。pid>0(等待指定的子进程),pid=-1(等待任意子进程),其他的有需要再看。
options选项中:0为阻塞等待,WNOHANG为非阻塞式.WUNTRACED:等待被暂停的子进程。WCONTINUED:等待被继续的子进程。
3.accept在主进程中接收客户端链接,会被信号打断,生成EINTR.这个要特殊处理一下,保证accept跳出后返回。

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
const int PORT = 8080;
//信号处理函数
void signal_handler(int signum) {
     while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);//设置非阻塞
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}
int main() {
    //添加信号机制处理子进程回收
    struct sigaction sa;
    sa.sa_handler = signal_handler;// 设置信号处理函数
    sigemptyset(&sa.sa_mask);//// 清空信号掩码
    sa.sa_flags = 0;                // 默认标志
    //注册信号捕捉
    sigaction(SIGCHLD, &sa, NULL);//SIGCHLD子进程结束时,父进程会收到这个信号



    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
    while (1) {
        int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
        if (cfd == -1) {
            if (errno == EINTR) {
                std::cout << "EINTR"<<std::endl;
                continue; 
            }
            perror("accept");
            exit(-1);
        }
        //尝试多进程必须要用到exit(0)以及wait(0)
        int pid = fork();
        if( pid < 0) {
            perror("fork");
            exit(-1);
        } else if (pid == 0) {//进入子进程
            //进入子进程,完全继承父进程的所有内存信息
            //尝试打印客户端信息,端口号+ip地址
            std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;
            //读取客户端信息
            char buf[1024];
            while (1) { 
                int readLen = read(cfd, buf, sizeof(buf));
                if (readLen > 0) {
                    //打印读到的信息
                    buf[readLen] = '\0';
                    printf("%s",buf);
                } else if(readLen == 0) {
                    //客户端已关闭连接
                    close(cfd);
                    break;//退出与客户端的信息交互
                } else {
                    perror("read");
                    close(cfd);
                    exit(-1);
                }
                //读到数据后进行回传
                const char* message = "this is server:";
                int writeLen = write(cfd, message,strlen(message));
                if (writeLen < 0) {
                    perror("write");
                    close(cfd);
                    exit(-1);
                }
                writeLen = write(cfd, buf,readLen);
                if (writeLen < 0) {
                    perror("write");
                    close(cfd);
                    exit(-1);
                }
            }
            exit(0);
        }
    }
    close(lfd);
    return 0;
}

多线程服务器代码

1.memcpy和直接赋值的区别?
memcpy不考虑数据类型,只考虑源地址和目的地址以及字节数量
直接赋值需要左右两边的数据类型一致,否则可能出现错误

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>//多线程 Compile and link with -pthread
const int PORT = 8080;
//定义结构体存储与客户端连接后的通信cfd;
struct sockInfo {
    int cfd;
    sockaddr_in addr;
    pthread_t tid;
};
sockInfo sockInfos[4];//定义4个线程
//定义线程函数void *(*start_routine) (void *)返回值为void* ,输入为void*的线程函数
void* communicateTackle(void* arg) {
    //线程函数,和客户端通信
    //入口参数为accept获取到的信息
    sockInfo* clientInfo = (sockInfo*)arg;
    //打印客户端信息
    in_port_t sinport = clientInfo->addr.sin_port;
    char clientAddress[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &clientInfo->addr.sin_addr.s_addr, clientAddress, sizeof(clientAddress));
    printf("clientport:%d, clientaddress: %s\n",sinport,clientAddress);
    //开始和客户端进行沟通
    int cfd = clientInfo->cfd;
    char buf[1024];
    while (1) { 
        int readLen = read(cfd, buf, sizeof(buf));
        if (readLen > 0) {
            //打印读到的信息
            buf[readLen] = '\0';
            printf("%s",buf);
        } else if(readLen == 0) {
            //客户端已关闭连接
            close(cfd);
            clientInfo->cfd = -1;
            break;//退出与客户端的信息交互
        } else {
            perror("read");
            close(cfd);
            clientInfo->cfd = -1;
            pthread_exit((void*)-1);
        }
        //读到数据后进行回传
        const char* message = "this is server:";
        int writeLen = write(cfd, message,strlen(message));
        if (writeLen < 0) {
            perror("write");
            close(cfd);
            clientInfo->cfd = -1;
            pthread_exit((void*)-1);
        }
        writeLen = write(cfd, buf,readLen);
        if (writeLen < 0) {
            perror("write");
            close(cfd);
            clientInfo->cfd = -1;
            pthread_exit((void*)-1);
        }
    }    
    pthread_exit(0);//等价于return NULL
    //用线程的话,可以分离线程,即父线程不用处理子线程
}

int main() {
    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
    //初始化线程相关信息
        // 初始化数据
    int max = sizeof(sockInfos) / sizeof(sockInfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockInfos[i], sizeof(sockInfos[i]));//全部置0
        sockInfos[i].cfd = -1;
        sockInfos[i].tid = -1;
    }
    while (1) {
        int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
        if (cfd == -1) {
            if (errno == EINTR) {
                std::cout << "EINTR"<< std::endl;
                continue; 
            }
            perror("accept");
            exit(-1);
        }
        //创建线程
        sockInfo* clientInfo = NULL;
        for (int i = 0; i < max; i++) {
            if (sockInfos[i].cfd == -1) {//说明这个结构体可用
                clientInfo = &sockInfos[i];
                break;
            }
            //处理线程耗尽问题
            if (i == max - 1) {
                printf("no vaild thread!!!\n");
                sleep(1);
                i=-1;
            }
        }
        clientInfo->cfd = cfd;
        //为什么使用memcpy?为什么不直接赋值
        memcpy(&clientInfo->addr,&sockaddrClient,addrlen);
        int ispcreateOk = pthread_create(&clientInfo->tid, NULL,
                          communicateTackle,clientInfo);
        if (ispcreateOk !=0) {
            perror("pthread_create");
            exit(-1);
        }
        //分离线程,线程回收不需要父进程处理,否则需要用pthread_join等待(会造成阻塞影响主进程)
        pthread_detach(clientInfo->tid);
    }
    close(lfd);
    return 0;
}

select/poll/epoll理解

1.select:最大同时支持1024个连接;每次调用select都要把设定的监听描述符信息fd_set从用户空间搬运到内核空间,再从内核空间搬运到用户空间;
fd_set也不能重用;内核需要遍历指定范围内的文件描述符。
2.poll:支持任意数量连接(数组大小需要提前设置);每次调用 poll 都需要将 struct pollfd 数组从用户空间复制到内核空间,返回时再复制回用户空间;同样内核需要遍历指定范围内的文件描述符。
3.epoll:支持任意数量连接(内核空间操作);文件描述符的管理在内核中完成,减少了用户空间和内核空间之间的数据拷贝。内核通过回调机制通知就绪事件,时间复杂度为 O(1);支持水平和边缘触发模式
4.性能最好的是epoll,不用考虑太多,直接用epoll

API调用:
select:
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
// 将参数文件描述符fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
 // 判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
int  FD_ISSET(int fd, fd_set *set);
 // 将参数文件描述符fd 对应的标志位,设置为1
void FD_SET(int fd, fd_set *set);
// fd_set一共有1024 bit, 全部初始化为0
void FD_ZERO(fd_set *set);
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int 
timeout);

IO多路复用:select

可同时检测的文件描述符数量最大为1024,实际场景中高并发用不上

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/select.h> //selectio多路复用
const int PORT = 8080;
int main() {
    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的

    //定义select相关参数
    fd_set readfds, readfdsTemp;//fdset是一个结构体,里面包含1024个bit,对应1024个文件描述符
    FD_ZERO(&readfds);//全部置0
    FD_SET(lfd, &readfds);//将监听文件描述符假如到readfds列表当中去
    int maxfd = lfd;//必然是申请的最小的文件描述符(0,1,2是标准输入,标准输出,标准错误,申请文件描述符默认申请的是当前未使用的最小的文件描述符)
    while (1) {
        readfdsTemp = readfds;
        //select IO多路复用,不检测可写与异常,永久阻塞,直到文件描述符发生了变化
        int changeNum = select(maxfd + 1, &readfdsTemp, NULL,NULL, NULL);
        if (changeNum == -1) {perror("select"); exit(-1);}
        else if (changeNum == 0) continue;//返回0是超时,此时设置的是永久阻塞,应该不会出现这个选项
        else {
            //开始检测是否有客户端连接(先处理和客户端的连接,再处理和已连接的客户端之间的通信)
            if (FD_ISSET(lfd,&readfdsTemp)) {
                //已经有客户端接入了,此时accept不会发生阻塞
                int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的第一个客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
                if (cfd == -1) {
                    if (errno == EINTR) {
                        std::cout << "EINTR";
                        continue; 
                    }
                    perror("accept");
                    exit(-1);
                }
                //尝试打印新加入的客户端信息,端口号+ip地址
                std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;
                FD_SET(cfd,&readfds);
                maxfd = maxfd > cfd ? maxfd : cfd;
            }//与新的客户端构成连接
            //接下来处理和已连接客户端之间的通信
            for (int i = lfd + 1; i <= maxfd; i++) {
                if (FD_ISSET(i, &readfdsTemp)) {
                    //与客户端进行通信
                    char buf[1024];
                    int readLen = read(i, buf, sizeof(buf));
                    if (readLen > 0) {
                        //打印读到的信息
                        buf[readLen] = '\0';
                        printf("%s",buf);
                    } else if(readLen == 0) {
                        //客户端已关闭连接
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    } else {
                        perror("read");
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    }
                    //读到数据后进行回传
                    const char* message = "this is server:";
                    int writeLen = write(i, message,strlen(message));
                    if (writeLen < 0) {
                        perror("write");
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    }
                    writeLen = write(i, buf,readLen);
                    if (writeLen < 0) {
                        perror("write");
                        close(i);
                        FD_CLR(i,&readfds);
                        continue;
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

IO多路复用:poll

可同时检测的文件描述符数量可以任意指定
代码中nfds的取值可能有点问题

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <poll.h>
const int PORT = 8080;
#define MAXPOLLNUM 10
int main() {
    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
    
    pollfd pollfds[MAXPOLLNUM];//poll的特点:可以指定任意大小,相比select的1024限制更大一些
    //初始化
    for (int i = 0; i < MAXPOLLNUM; i++) {
        pollfds[i].fd = -1;
        pollfds[i].events = POLLIN;
        pollfds[i].revents = 0;
    }
    pollfds[0].fd = lfd;//监听描述符
    int nfds = 0;//这个是第一个参数数组中最后一个有效元素的下标,设置的0为当前最后有效元素的下标
    while (1) {
        //nfds: 这个是第一个参数数组中最后一个有效元素的下标 + 1
        int changeNum = poll(pollfds, nfds + 1, -1);//阻塞监听
        if (changeNum == -1) {perror("poll"); exit(-1);}
        else if (changeNum == 0) continue;//阻塞一定时间后没有监听到任何描述符,前面-1已经设置了永久阻塞,这一步是不会发生的
        else {
            //首先检测是否有客户端连接到来,之后再处理已经连接的客户端通信
            if (pollfds[0].revents & POLLIN) {
                //已经有客户端接入了,accept不会阻塞
                int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
                if (cfd == -1) {
                    if (errno == EINTR) {
                        std::cout << "EINTR";
                        continue; 
                    }
                    perror("accept");
                    exit(-1);
                }
                std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;
                //将新连接加入到监听列表中
                for (int i = 1; i < MAXPOLLNUM; i++) {
                    if (pollfds[i].fd == -1) {
                        pollfds[i].fd = cfd;
                        pollfds[i].events = POLLIN;
                        pollfds[i].revents = 0;
                        nfds = nfds > cfd ? nfds : cfd;
                        break;
                    }
                }
            }
            //开始处理和已连接客户端的通信
            for (int i = 1; i < nfds; i++) {
                if (pollfds[i].revents & POLLIN) {
                    //开始处理通信
                    int cfd = pollfds[i].fd;
                    char buf[1024];
                    int readLen = read(cfd, buf, sizeof(buf));
                    if (readLen > 0) {
                        //打印读到的信息
                        buf[readLen] = '\0';
                        printf("%s",buf);
                    } else if(readLen == 0) {
                        //客户端已关闭连接
                        close(cfd);
                        pollfds[i].fd = -1;
                        continue;
                    } else {
                        perror("read");
                        close(cfd);
                        pollfds[i].fd = -1;
                        continue;
                    }
                    //读到数据后进行回传
                    const char* message = "this is server:";
                    int writeLen = write(cfd, message,strlen(message));
                    if (writeLen < 0) {
                        perror("write");
                        close(cfd);
                        pollfds[i].fd = -1;
                        continue;
                    }
                    writeLen = write(cfd, buf,readLen);
                    if (writeLen < 0) {
                        perror("write");
                        close(cfd);
                        pollfds[i].fd = -1;
                        continue;
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

IO多路复用:epoll

//TCP构建服务端
/*
1.socket,得到监听fd
2.bind,绑定监听fd
3.listen 开始监听
4.accept 阻塞接收,得到一个新的fd(独立与客户端进行通信的文件描述符)
5.recv/read
6.send/write
7.close
求助:man [函数名]
宗旨: 名称尽量起简洁点
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/epoll.h>
const int PORT = 8080;
#define MAXPOLLNUM 10
int main() {
    //第一步:创建监听套接字,得到监听文件描述符lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        exit(-1);
    }
    //端口复用,允许多个套接字绑定相同的ip地址和端口,服务器测试时频繁重启可以非常方便(用于快速重启服务器)
    int opt = 1;
    int isSetOk = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt,sizeof(opt));
    if (isSetOk == -1) {
        perror("setsockopt");
        exit(-1);
    }
    //第二步:将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息),服务器向客户端发送信息的源IP地址和源端口号就是这个
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = INADDR_ANY;//内核会自动将套接字绑定到所有可用的网络接口
    //int isTransOk = inet_pton(AF_INET,"127.0.0.1",(void*)&sockaddrServer.sin_addr.s_addr);//本地回环地址
    //if (isTransOk != 1) perror("inet_pton");
    //sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//虚拟网卡
    int isBindOk = bind(lfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isBindOk != 0) {
        perror("bind");
        exit(-1);
    }
    //第三步:监听:backlog 它控制着有多少个客户端连接请求可以被暂时保存,直到服务器调用 accept 接受这些连接。
    //如果客户端申请的队列超过8个,就会拒绝连接,使得客户端收到ECONNREFUSED错误
    int isListenOk = listen(lfd, 8);//表示等待连接队列的最大长度为8
    if (isListenOk != 0) {
        perror("listen");
        exit(-1);
    }
    //第四步:接收客户端连接
    sockaddr_in sockaddrClient;
    socklen_t addrlen = sizeof(sockaddrClient);//这个必须被初始化,要不然接入的ip是随机生成的
    
    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);//返回操作epoll实例的文件描述符
    if (epfd == -1) {
        perror("epoll_create");
        exit(-1);
    }
    //初始化,先把监听套接字加入进去
    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = lfd;
    // 对epoll实例进行管理:添加文件描述符信息,删除信息,修改信息
    isSetOk = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &event);
    if (isSetOk == -1) {perror("epoll_ctl"); exit(-1);}
    epoll_event epoll_events[MAXPOLLNUM];
    while (1) {
        //nfds: 这个是第一个参数数组中最后一个有效元素的下标 + 1
        int changeNum = epoll_wait(epfd, epoll_events, MAXPOLLNUM, -1);//阻塞监听
        if (changeNum == -1) {perror("epoll_wait"); exit(-1);}
        else if (changeNum == 0) continue;//阻塞一定时间后没有监听到任何描述符,前面-1已经设置了永久阻塞,这一步是不会发生的
        else {
            //首先检测是否有客户端连接到来,之后再处理已经连接的客户端通信
            for (int i = 0; i < changeNum; i++) {
                int curfd = epoll_events[i].data.fd;
                if (curfd == lfd) {
                    int cfd = accept(lfd, (sockaddr*)&sockaddrClient, &addrlen);//接收监听队列上的客户端连接,并建立一个新的文件描述符用于和该客户端进行连接
                    if (cfd == -1) {
                        if (errno == EINTR) {
                            std::cout << "EINTR";
                            continue; 
                        }
                        perror("accept");
                        exit(-1);
                    }
                    std::cout << "client port:" << ntohs(sockaddrClient.sin_port) << "\n" << "client ipaddress:" << inet_ntoa(sockaddrClient.sin_addr) << std::endl;
                    //将新的客户端连接注册进去
                    event.events = EPOLLIN;
                    event.data.fd = cfd;
                    int isSetOk = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
                    if (isSetOk == -1) {perror("epoll_ctl"); exit(-1);}
                } else {
                    //处理已连接客户端的通信
                    char buf[1024];
                    int readLen = read(curfd, buf, sizeof(buf));
                    if (readLen > 0) {
                        //打印读到的信息
                        buf[readLen] = '\0';
                        printf("%s",buf);
                    } else if(readLen == 0) {
                        //客户端已关闭连接
                        close(curfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);//从实例中删除该客户端连接的文件描述符
                        continue;
                    } else {
                        perror("read");
                        close(curfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                        continue;
                    }
                    //读到数据后进行回传
                    const char* message = "this is server:";
                    int writeLen = write(curfd, message,strlen(message));
                    if (writeLen < 0) {
                        perror("write");
                        close(curfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                        continue;
                    }
                    writeLen = write(curfd, buf,readLen);
                    if (writeLen < 0) {
                        perror("write");
                        close(curfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                        continue;
                    }
                } 
            }
        }
    }
    close(lfd);
    return 0;
}

连续打印客户端代码

//构建客户端
//TCP构建客户端
/*
1.socket,得到监听fd
2.connect建立连接
3.send/write
4.recv/read
7.close
*/
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h> //包含了结构体struct sockaddr_in,定义IPV4地址和端口号
#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
const int PORT = 8080;
int main() {
    //第一步:创建套接字用户通信
    int cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd < 0) {
        perror("socket");
        exit(-1);
    }
    //连接服务器:需要指定连接的服务器ip和端口
    sockaddr_in sockaddrServer;
    sockaddrServer.sin_port = htons(PORT);//端口号
    sockaddrServer.sin_family = AF_INET;
    sockaddrServer.sin_addr.s_addr = inet_addr("192.168.189.129");//以这个ip地址访问服务器,其实就是本机的虚拟网卡IP
    int isConnectOK = connect(cfd, (sockaddr*)&sockaddrServer, sizeof(sockaddrServer));
    if (isConnectOK == -1) {
        perror("connect");
        exit(-1);
    }
    char buf[1024];
    int num = 0;
    while (1) {
        num++;
        sprintf(buf,"this is count:%d\n",num);
        //fgets读完之后buf的末尾会有一个\n需要特别注意一下
        int writeLen = send(cfd, buf,strlen(buf),0);
        if (writeLen < 0) {
            perror("send");
            break;
        }
        sleep(3);
        int readLen = recv(cfd, buf, sizeof(buf),0);
        if (readLen > 0) {
            //打印读到的信息
            buf[readLen] = '\0';
            printf("%s",buf);
        } else if(readLen == 0) {
            //客户端已关闭连接
            close(cfd);
            break;//退出与客户端的信息交互
        } else {
            perror("recv");
            break;
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值