Linux网络编程套接字

预备知识

理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址

源IP地址: 表示该条信息来源于哪个机器。
目的IP地址: 表示该条信息去往于哪个进程

理解 “端口号” 和 “进程ID”

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
  • 一个端口号只能被一个进程占用
  • 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。就是在描述 “数据是谁发的, 要发给谁”

源端口号: 表示该条信息来源于哪个进程。
目的端口号: 表示该条信息去往于哪个机器。

认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,后面再详细讨论

  • 传输层协议
  • 有连接: 双方在发送网络数据之前必须建立连接,再进行发送
  • 可靠传输: 保证数据是可靠并且有序的到达对端,例如发送123、456时123数据先到达,456数据后到达。即使456数据先到达传输层,也会阻塞等待前面的数据123先到达。
  • 面向字节流: TCP发送数据的单位是以字节为单位,并且数据没有明显的边界例如:123456数据不会分开

认识UDP协议

这里我们也先对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;,后面再详细讨论

  • 传输层协议
  • 无连接: 双方在发送网络数据之前不需要建立连接,直接发送,客服端不用管服务端是否在线
  • 不可靠传输: UDP并不会保证数据有序的到达对端
  • 面向数据报: UDP不管向应用层还是网络层传递数据都是整条数据

对比一下TCP和UDP

TCP是可靠传输,有连接,字节流
UDP是不可靠传输,无连接,数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

在这里插入图片描述

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

#include<arpa/inet.h>

uint16_t htons(uint16_t hostshort)
uint32_t htonl(uint32_t hostlong)

uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong)

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket结构

  1. socket常见API(接口)
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr * address,socklen_t address_len);*
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr*addr,socklen_t addrlen);
  1. sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX Domain Socket。然而, 各种网络协议的地址格式并不相同。

在这里插入图片描述

  • IPv6把IP地址由32位增加到128位,从而能够支持更大的地址空间
  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr * 类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
  1. sockaddr 结构

在这里插入图片描述
sockaddr 结构用于存储参与(IP)Windows/linux套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。这是由于Microsoft TCP/IP套接字开发人员的工具箱仅支持internet地址字段,而实际填充字段的每一部分则遵循sockaddr_in数据结构,两者大小都是16字节,所以二者之间可以进行切换。

  1. sockaddr_in 结构
    在这里插入图片描述
    虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

  2. in_addr结构

在这里插入图片描述
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

socket编程接口

下面介绍程序中用到的socket 接口,这些函数都在sys/socket.h中

socket()

在这里插入图片描述
功能: 打开一个网络通讯端口,打开后应用程序可以像读写文件一样用read/write在网络上收发数据

函数原型:

int socket(int domain,int type,int protocol);

参数:

  • domain:协议域,对于IPv4, family参数指定为AF_INET。
  • type:socket的类型,流格式套接字(SOCK_STREAM)、数据报格式套接字(SOCK_DGRAM)。
  • protocol:协议,当protocol为0时,会自动选择type类型对应的默认协议。

返回值:
如果成功的话,就像open()一样返回一个文件描述符,调用出错则返回-1

bind()

在这里插入图片描述

介绍: 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号,bind函数的功能就是将参数sockfd和addr绑定在一起

函数原型:

int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

参数:

  • sockfd:创建的套接字,即socket函数返回的文件描述符,唯一标识一个socket
  • addr:sockaddr结构体
  • addrlen:struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度

返回值: bind()成功返回0,失败返回-1

我们的程序中对addr参数是这样初始化的

bzero (&servaddr, sizeof(servaddr)) ;// 将整个结构体清零
servaddr.sin_family = AF_INET;// 设置地址类型为AF_INET
servaddr.sin_addr.s _addr= hton1(INADDR_ANY);// 网络地址为INADDR_ANY, 
// 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 
// 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址
servaddr.sin port = htons(SERV_PORT);// 端口号为SERV_PORT

listen()

在这里插入图片描述
功能:
声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略。

函数原型:

int listen(int sockfd,int backlog);

参数:

  • sockfd:创建的套接字
  • backlog:最多允许客户端处于连接等待状态的个数(通常设置为5)

返回值: 成功返回0,失败返回-1

accept()

在这里插入图片描述

功能: 在一个套接口接受一个连接,三次握手完成后, 服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

函数原型:

int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);

参数:

  • sockfd:创建的套接字
  • addr:addr是一个传出参数,accept()返回时传出客户端的地址和端口号,如果给addr 参数传NULL,表示不关心客户端的地址
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)

返回值: 成功返回非负值,失败返回-1

服务器程序结构是这样的

while(1){
	cliaddr_len = sizeof(cliaddr);
	connfd =accept (listenfd,(struct sockaddr*) &cliaddr, &cliaddr_len) ;
	n = read (connfd, buf,MAXLINE);
	...
	close(connfd);
}

connect

在这里插入图片描述

功能: 客户端需要调用connect()连接服务器

函数原型:

int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

参数:

connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址

返回值: connect()成功返回0,出错返回-1

UDP协议使用

UDP网络通信程序

下面实现一个简单的网络通信程序

客户端文件 udp_client.cc

#include<iostream>
#include<sys/types.h>// socket头文件
#include<sys/socket.h>// socket头文件
#include<cstdio>
#include<cstring>// memset头文件
#include<arpa/inet.h>// htons,inet_addr头文件
#include<netinet/in.h>// inet_addr头文件
#include<unistd.h>// read和close头文件

void Usage(std::string proc)
{
    std::cerr<<"Usage: "<<"\n\t"<<proc<<" desc_ip desc_port"<<std::endl;
}

int main(int argc,char* argv[])
{
    if(argc!=3){
        Usage(argv[0]);
        return 1;
    }

    int sock=socket(AF_INET,SOCK_DGRAM,0);// 创建套接字
    if(sock<0){
        std::cerr<<"socket error"<<std::endl;
        return 2;
    }

    // 不需要用户去主动bind,实际上,在sendto的时候,OS会自动随机给client bind端口号

    char buffer[1024];

    struct sockaddr_in desc;
    memset(&desc,0,sizeof(desc));
    desc.sin_family=AF_INET;
    desc.sin_port=htons(atoi(argv[2]));
    desc.sin_addr.s_addr=inet_addr(argv[1]);

    for( ; ; )
    {
        std::cout<<"Please Enter# "<<std::endl;
        fflush(stdout);// 刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上
        buffer[0]=0;
        ssize_t size=read(0,buffer,sizeof(buffer)-1);// 将输入读取到buffer上
        if(size>0){
            buffer[size-1]=0;// 确定读取数据的末尾

            // 将buffer中的数据通过sock传给对方主机
            sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc/*发送到哪里*/,sizeof(desc)/*长度*/);

            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            // 通过sock接收数据
            ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);// peer,len 暂时不用
            if(s>0){
                buffer[s]=0;
                std::cout<<"#echo "<<buffer<<std::endl;
            }
        }
    }

    close(sock);// 关闭套接字
    return 0;
}

服务端文件 udp_server.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>// memset头文件
#include<unistd.h>// read和close头文件
#include<time.h>
#include<arpa/inet.h>// htons,inet_addr头文件
#include<netinet/in.h>// inet_addr头文件

#define PORT 8080

void Usage(std::string proc)
{
    std::cerr<<"Usage: "<<"\n\t"<<proc<<" local_port"<<std::endl;
}

int main(int argc,char *argv[])
{
    if(argc!=2){
        Usage(argv[0]);
        return 1;
    }
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock<0){
        std::cerr<<"socket error"<<std::endl;
        return 2;
    }
    std::cout<<"sock: "<<std::endl;
    
    //该结构是OS给你提供的一个结构体,用户层定义的,local是属于main函数内的一个临时变量
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[1]));// 后续网络端口,会以源端口的方式,发送给对面

    // 注意: 云服务器你要bind的时候的,一般不能直接绑定明确的IP
    // 推荐使用INADDR_ANY, bind所有你的机器上面的ip
    //一个IP本质,可以使用4个字节进行保存[0-255].[0-255].[0-255].[0-255], "42.192.83.143",点分十进制字符串风格IP
    local.sin_addr.s_addr=htons(INADDR_ANY);

    //就是将本主机相关的ip,端口,协议家族等信息写入到特定的fd标定的文件中
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
        std::cerr<<"bind error"<<std::endl;
        return 1;
    }

    char message[1024];
    for( ; ; )
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        // 通过sock接收数据
        ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len);
        if(s>0){
            message[s]='\0';
            std::cout<<"client# "<<message<<std::endl;
            // 给数据添加时间戳
            std::string echo_message = message;
            echo_message += "_server_";
            echo_message += std::to_string((long long)time(nullptr));

            sendto(sock,echo_message.c_str(),echo_message.size(),0,(struct sockaddr*)&peer,len);
        }

    }
    close(sock);// 关闭套接字
    return 0;
}

在这里插入图片描述
关于绑定的一些问题

Server端,为何要明确bind ?
client:server = n:1, server给别人提供服务,就需要自己尽可能的将自己暴露出去(IP(域名)+PORT(一般是被隐藏的)),必须是“稳定”(不能轻易改变,尤其是端口号)的。

client为何不需要明确bind?
如果client没有port,也变无法与server进行通信。
为何不需要我们给他bind呢?
如果你自己bind了,成功了还好,如果你的client端口被别的程序占用,你的client就无法启动,客户端不是一定要用哪一个端口,只需要有一个端口就可以。我们一般不自己bind,而是由OS随机帮我们查找端口。

TCP协议使用

查看tcp相关信息可以用如下指令 netstat -nltp

  1. n能显示数字
  2. l只查看listen状态的接口
  3. t查看tcp链接
  4. p查看到与tcp服务相关的进程信息

在这里插入图片描述

TCP网络通信程序

下面编写TCP网络程序实现通信

任务处理文件 handler.hpp

#pragma once

#include"tcp_server.hpp"

namespace ns_handler{
    using namespace ns_tcpserver;

#define SIZE 1024

    void HandlerHelper(int sock)
    {
        while(true)
        {
            char buffer[1024];
            ssize_t s = read(sock,buffer,sizeof(buffer)-1);
            if(s>0){
                // read success
                buffer[s]=0;
                std::cout<<"client# "<<buffer<<std::endl;
                std::string echo_string = buffer;

                if(echo_string == "quit"){
                    break;
                }

                echo_string += "[sever say]";
                write(sock,echo_string.c_str(),echo_string.size());
            }
            else if(s==0){
                //对端链接关闭
                std::cout<<sock<<" : client quit ... "<<std::endl;
                break;
            }
            else{
                // 读取失败
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }

    void HandlerSock_V1(int sock) 
    {
        HandlerHelper(sock); 
    }
}

tcp客户端头文件 tcp_client.hpp

#pragma once

#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<strings.h>


namespace ns_tcpclient{
    class TcpClient{
        private:
            std::string desc_ip;// client要访问的对端服务器的IP地址
            uint16_t desc_port;//client要访问的对端服务器的port端口号
            int sock;

        public:
            TcpClient(std::string _ip,uint16_t _port):desc_ip(_ip),desc_port(_port),sock(-1)
            {}

            void InitTcpClient()
            {
                //创建socket
                sock=socket(AF_INET,SOCK_STREAM,0);
                if(sock<0){
                    std::cerr<<"socket error"<<std::endl;
                    exit(2);
                }

                //2. client要不要bind??不要自己进行bind!在你发起链接的时候,OS会自动给你进行相关的绑定!
                //3. client要不要listen?不需要!
                //4. client要不要accept?不需要!              
            }

            // tcp是面向连接的 client 通信前必须建立连接

            void Start()
            {
                // 填充对方服务器的socket信息
                struct sockaddr_in svr;
                bzero(&svr,sizeof(svr));
                svr.sin_family = AF_INET;
                svr.sin_port = htons(desc_port);
                svr.sin_addr.s_addr = inet_addr(desc_ip.c_str());

                // 发起链接请求
                if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))==0){
                    std::cout<<"connect success..."<<std::endl;
                }
                else{
                    std::cout<<"connect failed..."<<std::endl;
                }

                // 完成业务逻辑
                while(true)
                {
                    char buffer[1024]={0};
                    std::cout<<"请你输入# ";
                    fflush(stdout);

                    ssize_t s = read(0,buffer,sizeof(buffer)-1);
                    if(s>0){
                        buffer[s-1]=0;
                        write(sock,buffer,strlen(buffer));

                        size_t  rs = read(sock,buffer,sizeof(buffer)-1);
                        if(rs > 0){
                            buffer[rs]=0;
                            std::cout<<buffer<<std::endl;
                        }
                        else{
                            std::cout<<"server close..."<<std::endl;
                        }
                    }
                }
            }

            ~TcpClient()
            {
                if(sock>=0) close(sock);
            }
    };
}

客户端主文件 client.cc

#include"tcp_client.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage: "<<"\n\t"<<proc<<" svr_ip svr_port"<<std::endl;
}

int main(int argc,char* argv[])
{
    if(argc != 3){
        Usage(argv[0]);
        return 1;
    }

    std::string ip=argv[1];
    uint16_t port = atoi(argv[2]);

    ns_tcpclient::TcpClient cli(ip,port);

    cli.InitTcpClient();

    cli.Start();

    return 0;
}

服务端头文件 tcp_server.hpp

#pragma once

#include<iostream>
#include<strings.h>// bzero头文件
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

namespace ns_tcpserver{

    typedef void(*handler_t)(int);//函数指针类型

    const int backlog = 5;

    class TcpServer{
        private:
            uint16_t port;
            int listen_sock;// 获取新链接

        public:
            TcpServer(int _port):port(_port),listen_sock(-1)
            {}

            void InitTcpServer()
            {
                listen_sock = socket(AF_INET,SOCK_STREAM,0);
                if(listen_sock < 0){
                    std::cout<<"socket error"<<std::endl;
                    exit(2);
                }
                struct sockaddr_in local;
                bzero(&local,sizeof(local));

                local.sin_family = AF_INET;
                local.sin_port = htons(port);
                local.sin_addr.s_addr = INADDR_ANY;

                if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0){
                    std::cerr<<"bind error"<<std::endl;
                    exit(3);
                }

                //3. 监听,tcp协议是面向连接的,即如果要正式传递数据之前,需要先建立链接
                //目的:允许client来连接server
                if(listen(listen_sock,backlog)<0){
                    std::cerr<<"listen error"<<std::endl;
                    exit(4);
                }
            }

            void Loop(handler_t hander)
            {
                while(true){
                    struct sockaddr_in peer;
                    socklen_t len = sizeof(peer);
                    // 1.获取链接
                    int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
                    if(sock<0){
                        std::cout<<"warning: accept error"<<std::endl;
                        continue;
                    }

                    // debug,验证一下fd值
                    std::cout<<"debug: sock->"<<sock<<std::endl;
                    uint16_t peer_port = ntohs(peer.sin_port);
                    std::string peer_ip = inet_ntoa(peer.sin_addr);// 4字节ip->点分十进制字符串风格的IP

                    //验证一下对端的socket信息中,ip,port
                    std::cout << "debug: " << peer_ip << ":" << peer_port << std::endl;

                    // 处理链接
                    hander(sock);
                    // 关闭链接
                    close(sock);                   
                }
            }

            ~TcpServer()
            {
                if(listen_sock>=0) close(listen_sock);
            }
    };
}

服务端主文件 server.cc

#include"tcp_server.hpp" //提供网络连接功能
#include"handler.hpp" //提供网络sock的处理功能

static void Usage(std::string proc)
{
    std::cerr<<"Usage: "<<"\n\t"<<proc<<" prot"<<std::endl;
}

int main(int argc,char* argv[])
{
    if(argc!=2){
        Usage(argv[0]);
        return 0;
    }
    uint16_t port=atoi(argv[1]);
    ns_tcpserver::TcpServer* svr=new ns_tcpserver::TcpServer(port);

    svr->InitTcpServer();
    svr->Loop(ns_handler::HandlerSock_V1);

    return 0;
}

在这里插入图片描述
上面的程序一次只能有一个客户端和服务端通信,能不能让服务端一次和多个客户端通信呢?这里可以利用多进程

// 利用多进程
void HandlerSock_V2(int sock)
{
    if(fork()==0){
        // 子进程
        if(fork()>0){
            // 子进程
            exit(0);
        }
        // 孙子进程,孤儿进程,会被OS领养
        HandlerHelper(sock);
        exit(0);
    }
    // 父进程
    close(sock);
    waitpid(-1,nullptr,0);
}

这里利用了孙子进程,避免了子进程需要父进程等待的问题

在这里插入图片描述
进程创建的成本高,我们也可以创建线程

// 利用多线程
void* thread_routine(void* args)
{
    int sock=*(int*)args;
    delete (int*)args;

    pthread_detach(pthread_self());

    HandlerHelper(sock);//业务逻辑
    //已经处理完毕,需要关闭不需要的fd,如果不关闭,就造成了文件描述符泄漏!
    close(sock);

    return nullptr;
}
void HandlerSock_V3(int sock)
{
    // 多线程
    pthread_t tid;
    int *p=new int(sock);
    pthread_create(&tid,nullptr,thread_routine,p);
}

创建进程和线程的优缺点

多进程:链接来了,才创建进程,而且没有上限, 优点: 稳定,进程是具独立性的。缺点:进程创建和交互的成本高,效率变低。
多线程:链接来了,才创建线程,而且没有上限, 优点: 轻量化。缺点:健壮性不足(一个线程崩掉整个进程都会崩掉)一旦系统中的进程或者线程极度增多,进程或者线程在系统内切换的成本增加,切换的周期变长了,甚至导致系统宕机。

用TCP模拟英译汉服务器

该程序和上文的TCP通信程序类似,只是处理任务不同

std::unordered_map<std::string,std::string> dict{
    {"apple" , "苹果"},
    {"pear", "梨"},
    {"banana", "香蕉"},
    {"peach","桃"}
};

void HandlerHelper(int sock)
{
    while(true)
    {
        char buffer[1024];
        ssize_t s = read(sock,buffer,sizeof(buffer)-1);
        if(s>0){
            // read success
            buffer[s]=0;
            std::cout<<"client say# "<<buffer<<std::endl;
            std::string k = buffer;
            std::string message = "词库中没有该单词";
            auto iter = dict.find(k);
            if(iter!=dict.end()){
                message=iter->second;
            }
            write(sock,message.c_str(),message.size());
            std::cout << "server echo# " << message <<std::endl;
        }
    }
}

此外,再提供一个线程池版本

线程池头文件 thread_pool.hpp

#pragma once 

#include <iostream>
#include <queue>
#include <pthread.h>


template <class T>
class ThreadPool{
    private:
        std::queue<T> q; //给线程池派发任务的地点, 临界资源
        pthread_mutex_t lock;
        pthread_cond_t cond;
    private:
        ThreadPool()
        {
            pthread_mutex_init(&lock, nullptr);
            pthread_cond_init(&cond, nullptr);
        }
        ThreadPool(const ThreadPool<T>&) = delete;
        ThreadPool<T>& operator = (const ThreadPool<T>&) = delete;
        static ThreadPool<T> *instance;
    public:
        static ThreadPool<T> *get_instance(int num)
        {
            //static std::mutex lock;
            //lock.lock();
            static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
            if(nullptr == instance){
                pthread_mutex_lock(&mtx);
                if(nullptr == instance){
                    instance = new ThreadPool<T>();
                    instance->InitThreadPool(num);
                    //instance->Init...
                }
                pthread_mutex_unlock(&mtx);
            }
            //lock.unlock();
            return instance;
        }
        void LockQueue()
        {
            pthread_mutex_lock(&lock);
        }
        void UnlockQueue()
        {
            pthread_mutex_unlock(&lock);
        }
        bool IsEmpty()
        {
            return q.size() == 0;
        }
        void ThreadWait()
        {
            pthread_cond_wait(&cond, &lock);
        }
        void ThreadWakeup()
        {
            pthread_cond_signal(&cond);
        }
        void PopTask(T *out)
        {
            *out = q.front();
            q.pop();
        }
        //Routinue是类中的一个成员方法!包含了一个隐士参数this!ThreadPool*
        //实际上,这里是包含了两个参数的!
        static void *Routinue(void *args/*,ThreadPool *this*/)
        {
            pthread_detach(pthread_self()); //线程分离
            ThreadPool *tp = (ThreadPool*)args;

            while(true){
                tp->LockQueue();
                //1. 检测是否有任务
                //if -> while
                while(tp->IsEmpty()){
                    //thread 应该等待,等待有任务
                    tp->ThreadWait(); //我们线程当前是在临界区内等待的!我是持有锁的!!!
                }
                //2. 取任务的过程
                T t;
                tp->PopTask(&t);
                tp->UnlockQueue();
                //3. 处理任务, 拿到任务之后,处理任务的时候,需要在临界区内处理吗?不需要
                //在你的线程处理任务期间,其他线程是不是可以继续获取任务,处理任务
                t();
            }
        }
        void InitThreadPool(int num)
        {
            for(auto i = 0; i < num; i++){
                pthread_t tid;
                pthread_create(&tid, nullptr, Routinue, this);
            }
        }
        void PushTask(const T &in)
        {
            //放任务
            LockQueue();
            q.push(in);
            ThreadWakeup();
            UnlockQueue();
        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        }
};

template<class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;

线程池的应用

class task{
    private:
        int sock;
    public:
        task(){}
        task(int _sock):sock(_sock)
        {}
        void operator()()
        {
            std::cout << "当前处理的线程是:" << pthread_self() << std::endl;
            HandlerHelper(sock);
            close(sock);               
        }
        ~task(){}
};

//线程池
void HandlerSock_V4(int sock)
{
    ThreadPool<task>::get_instance(5)->PushTask(task(sock));
}

运行结果
在这里插入图片描述

TCP协议通讯流程

TCP协议通讯的前提是先将客户端和服务端连接起来,这里的连接是指什么呢?

客户端连接服务器的时候,本质上是连接了服务器的操作系统(协议栈)模块。服务端和客户端是1:n的关系,所以一定会有多个客户端去连接一个服务器OS。
此时服务器OS上会有大量的客户端连接。服务器OS需要将所有连接管理起来(先描述,再组织)。
所谓的连接本质就是在双方OS内,维护对应的数据结构,建立了连接,后序也要维护连接,所以,建立连接是有成本的,消耗了时间和空间。

断开连接的本质:释放双方建立好的数据结构

下图是基于TCP协议的客户端/服务器程序的一般流程:

在这里插入图片描述

服务器初始化:

  • 调用socket, 创建文件描述符
  • 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败
  • 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备
  • 调用accept, 并阻塞, 等待客户端连接过来

建立连接的过程:

  • 调用socket, 创建文件描述符
  • 调用connect, 向服务器发起连接请求
  • connect会发出SYN段并阻塞等待服务器应答; (第一次)
  • 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
  • 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为 三次握手

数据传输的过程

  • 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据
  • 服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待
  • 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答
  • 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求
  • 客户端收到后从read()返回, 发送下一条请求,如此循环下去

断开连接的过程:

  • 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)
  • 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)
  • read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
  • 客户端收到FIN, 再返回一个ACK给服务器; (第四次)

这个断开连接的过程, 通常称为 四次挥手

在学习socket API时要注意应用程序和TCP协议层是如何交互的:

  • 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
  • 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值