Linux网络-TCP/UDP套接字编程

2022-11-15 18:22:54 浏览数 (1)

零、前言

本章主要是对套接字网络编程的一个学习,目标是能够基本的进行套接字编程

一、UDP套接字

1、创建套接字

无论是服务端还是客户端,进行网络编程需要做的第一件事就是创建套接字

  • socket函数函数原型:
代码语言:javascript复制
int socket(int domain, int type, int protocol);
  • 解释:
  1. domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。填写struct sockaddr结构的前16位:本地通信设置为AF_UNIX,网络通信设置为AF_INET(IPv4)或AF_INET6(IPv6)
  2. type:套接字协议的传输类型:对于UDP的数据报式传输则填入SOCK_DGRAM,对于TCP的流式传输则填入SOCK_STREAM
  3. protocol:创建套接字的协议类别。可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,即默认(会根据前两个参数自动推导)
  4. 返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置
  • 示例:
代码语言:javascript复制
     //创建socket网络文件
    int sock=socket(AF_INET,SOCK_DGRAM,0);//ipv4协议,数据报式套接(UDP),套接字协议(0:默认协议)
    if(sock < 0)
    {
        std::cerr<<"socket"<<std::endl;
        return 2;
    }
    std::cout<<"sock:"<<sock<<std::endl;

2、填写ip/port和绑定

对于服务端和客户端都要进行绑定ip及port,只有绑定后才能标识网络中唯一的主机中的进程服务,便于进程接下来的数据传输

  • struct sockaddr_in成员:

sin_family:表示协议家族 sin_port:表示端口号,是一个16位的整数 sin_addr:表示IP地址,是一个32位的整数 sin_addr中的成员s_addr:表示IP地址,是一个32位的整数

  • 注意:
  1. 对于服务端来说,服务端ip和port需要被多个客户端所熟知的,所以服务端的port是需要进行固定化的,也就是说一个服务端的port是该服务端所私有的,不能随意更换
  2. 对于云服务器上的服务端,不建议绑定明确的ip,建议使用INADDR_ANY绑定该主机所有设备,以此接收向该主机发送的所有数据
  3. 对于客户端来说,客户端是不提供服务的,ip和port不用被其他主机熟知,并且为了启动客户端的顺利(固定的port被占用会使得进程启动不了),所以不需要我们主动去进行绑定ip和port,当进行数据的发送时,系统会自动绑定ip以及随机的port
  4. 对于客户端虽然不用主动填写自己的ip和port,但是需要的是明确数据传输的主机中的进程,即需要填写服务端的ip和port
  • IP格式转化:
  1. 对于进行绑定的网络信息字段是需要我们主动进行网络字节序的转化的,系统提供了相应的接口(上面介绍了),而发送的数据系统会在底层进行网络字节序的转化
  2. 在ip的转化时,我们习惯用的是点分十进制的字符串ip,例如192.168.233.123,但是需要填入的ip形式是四字节整数ip
  • inet_addr函数的函数原型:
代码语言:javascript复制
in_addr_t inet_addr(const char *cp);
  • 解释:
  1. 功能:将点分十进制的字符串IP转换成四字节整数IP
  2. 传入待转换的字符串IP,该函数返回的就是转换后的整数IP
  • inet_ntoa函数原型:
代码语言:javascript复制
char *inet_ntoa(struct in_addr in);
  • 解释:
  1. 将四字节整数IP转换成点分十进制字符串IP
  2. 传入inet_ntoa函数的参数类型是in_addr,不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可

注:上述函数在转化ip格式时同时也会自动进行网络字节序的转化

服务端创建套接字,即底层打开了对应的网络套接字文件,想进行网络通信还需要绑定对应的网络信息,即将套接字文件与网络进行强相关

  • bind函数函数原型:
代码语言:javascript复制
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 解释:
  1. sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符
  2. addr:网络相关的属性信息,包括协议家族、IP地址、端口号等
  3. addrlen:传入的addr结构体的长度
  4. 返回值说明:绑定成功返回0,绑定失败返回-1,同时错误码会被设置
  • 注意:
  1. 在绑定时需要将网络相关的属性信息填充到struct sockaddr_in结构体当中,然后将该结构体地址作为bind函数的第二个参数进行传入(这里需要强转为struct sockaddr *addr类型)
  2. UDP是数据报式套接字,并不会管对端的接收转态,只要绑定后就可以向对端进行接收消息了,但是这样的传输实际中是存有风险的
  • 示例:服务端
代码语言:javascript复制
 	//创建套接字结构体-填入ip及port
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));//初始化结构体
    local.sin_family=AF_INET;//通信协议-ipv4
    local.sin_port=htons(atoi(argv[1]));//使用命令行参数 网络字节序转化接口
    local.sin_addr.s_addr=htons(INADDR_ANY);//云服务器不建议绑定明确的ip,建议使用INADDR_ANY绑定该主机所有设备
	//将网络文件与套接字进行绑定(强相关)
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))==-1)
    {
        std::cout<<"bind"<<std::endl;
        return 3;
    }
  • 示例:客户端
代码语言:javascript复制
	//填入目标套接字的信息-确定传输数据的对象
    struct sockaddr_in desc;
    memset(&desc,sizeof(desc),0);
    desc.sin_family=AF_INET;//通信的ip协议-ipv4
    desc.sin_port=htons(atoi(argv[2]));//字符串转整数 网络字节序转化
    desc.sin_addr.s_addr=inet_addr(argv[1]);//点分十进制字符串ip转四字节整数ip-自动转化为网络字节序
 	//客户端不用主动绑定ip和port,当向远端发送消息是会自动绑定-服务端才需要固定的ip及port

3、数据发送和接收

  • sendto函数原型:
代码语言:javascript复制
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • 解释:
  1. sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
  2. buf:待写入数据的存放位置
  3. len:期望写入数据的字节数
  4. flags:写入的方式,一般设置为0,表示阻塞写入
  5. dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
  6. addrlen:传入dest_addr结构体的长度
  7. 返回值:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置

注:由于UDP不是面向连接的,所以传输数据时需要指明对端网络相关的信息,即sendto的最后两个参数用来表示对端的信息

  • recvfrom函数函数原型:
代码语言:javascript复制
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • 解释:
  1. sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据
  2. buf:读取数据的存放位位置
  3. len:期望读取数据的字节数
  4. flags:读取的方式,一般设置为0,表示阻塞读取
  5. src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
  6. addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数
  7. 返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置

注:recvfrom接口的倒数第二个参数是一个输出型参数,用于获取发送消息的对端网络信息,这样就知道是谁发的数据,并可以进一步向对端做出回应

  • 示例:服务端
代码语言:javascript复制
	//进行获取远端消息并回复
    while(1)
    {
        char buffer[128]={0};
        //接收远端的套接字信息-便于进行回复
        struct sockaddr_in peer; //接收对端信息
        socklen_t len=sizeof(peer);
        ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len); //接收数据
        if(s>0)
        {
            buffer[s]=0;
            std::cout<<"client# "<<buffer<<std::endl;
            sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);//回显传输
        }
    }
  • 示例:客户端
代码语言:javascript复制
	while(1)
    {
        std::cout<<"Please Enter# ";//提示符
        fflush(stdout);//刷新缓冲区
        char buffer[128]={0};
        ssize_t size=read(0,buffer,sizeof(buffer)-1);//保存键盘输入数据
        if(size>0)
        {
            buffer[size-1]=0;//覆盖回车符
            sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));//向服务端发送消息
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//接收消息
            if(s>0)
            {
                buffer[s]=0;
                std::cout<<"Echo# "<<buffer<<std::endl;
            }
        }
    }

4、简单回声服务器

  • 写一个简单的回声服务器:

当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sento函数将收到的数据重新发送给对应的客户端,以此测试双端的数据的收发功能

  • 服务端代码:
代码语言:javascript复制
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
//命令行参数的使用
int main(int argc,char* argv[])
{
    //程序启动
    if(argc!=2)
    {
        std::cerr<<"Usage: udpserver port"<<std::endl;
        return 1;
    }
    //创建socket网络文件
    int sock=socket(AF_INET,SOCK_DGRAM,0);//ipv4协议,数据报式套接,套接字协议(0:默认协议)
    if(sock < 0)
    {
        std::cerr<<"socket"<<std::endl;
        return 2;
    }
    std::cout<<"sock:"<<sock<<std::endl;
    //创建套接字结构体-填入ip及port
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));//初始化结构体
    local.sin_family=AF_INET;//通信协议-ipv4
    local.sin_port=htons(atoi(argv[1]));//使用命令行参数 网络字节序转化
    local.sin_addr.s_addr=htons(INADDR_ANY);//云服务器不建议绑定明确的ip,建议使用INADDR_ANY绑定该主机所有设备
    //将网络文件与套接字进行绑定(强相关)
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))==-1)
    {
        std::cout<<"bind"<<std::endl;
        return 3;
    }
    //进行获取远端消息并回复
    while(1)
    {
        char buffer[128]={0};
        //接收远端的套接字信息-便于进行回复
        struct sockaddr_in peer; 
        socklen_t len=sizeof(peer);
        ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len); 
        if(s>0)
        {
            buffer[s]=0;
            std::cout<<"client# "<<buffer<<std::endl;
            sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
        }
    }
    close(sock);
    return 0;
}
  • 客户端代码:
代码语言:javascript复制
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cerr<<"Usage: udp_client desc_ip desc_port"<<std::endl;
        return 1;
    }
    //创建网络文件
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock < 0)
    {
         std::cerr<<"Usage: udp_client desc_ip desc_port"<<std::endl;
    }
    //填入目标套接字的信息
    struct sockaddr_in desc;
    memset(&desc,sizeof(desc),0);
    desc.sin_family=AF_INET;//通信的ip协议-ipv4
    desc.sin_port=htons(atoi(argv[2]));//字符转数字-传输格式转化
    desc.sin_addr.s_addr=inet_addr(argv[1]);//点分十进制字符转网络-自动会将主机格式转为网络格式
    //客户端不用主动绑定ip和port,当向远端发送消息是会自动绑定-服务端才需要固定的ip及port
    while(1)
    {
        std::cout<<"Please Enter# ";
        fflush(stdout);
        char buffer[128]={0};
        ssize_t size=read(0,buffer,sizeof(buffer)-1);
        if(size>0)
        {
            buffer[size-1]=0;//覆盖回车符
            sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(s>0)
            {
                buffer[s]=0;
                std::cout<<"Echo# "<<buffer<<std::endl;
            }
        }
    }
    close(sock);
    return 0;
}
  • 运行效果:

二、TCP套接字

相比于UDP套接字来说,TCP套接字与之在一些地方是相同的,但是TCP的特点是面向链接的流式套接字,所以还是有很大的区别的

1、创建套接字

同样的tcp的服务端和客户端首先第一件事是创建套接字文件

  • socket函数函数原型:
代码语言:javascript复制
int socket(int domain, int type, int protocol);
  • 示例:
代码语言:javascript复制
     //1.创建socket网络文件
int sock=socket(AF_INET,SOCK_STREAM,0);//ipv4协议,流式套接(TCP),套接字协议(0:默认协议)
    if(sock < 0)
    {
        std::cerr<<"socket"<<std::endl;
        return 2;
    }
    std::cout<<"sock:"<<sock<<std::endl;

2、填写ip/port和绑定

tcp的服务端和客户端接下来的事也是填写ip/port和绑定

  1. 对于服务端来说,服务端ip和port需要被多个客户端所熟知的,所以服务端的port是需要进行固定化
  2. 对于客户端来说,客户端是不提供服务的,不用固定化port,不需要我们主动去进行绑定ip和port,当进行数据的发送时,系统会自动绑定ip以及随机的port
  3. 对于客户端虽然不用主动填写自己的ip和port,但是需要的是明确数据传输的主机中的进程,即需要填写服务端的ip和port
  • 示例:服务端
代码语言:javascript复制
        //2.绑定port号及ip地址
        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)
        {
            cerr<<"bind"<<endl;
            exit(2);
        }
  • 示例:客户端
代码语言:javascript复制
    //2.创建socketaddr_in结构体并填入服务端的信息
    struct sockaddr_in desc;
    bzero(&desc,sizeof(desc));
    desc.sin_family=AF_INET;//传输协议
    desc.sin_port=htons(desc_port);//端口号
    desc.sin_addr.s_addr=inet_addr(desc_ip.c_str());//IP地址-点分十进制转四字节ip同时转成网络传输格式
	//客户端并不用进行绑定自己的端口-发送数据时会自动进行绑定

3、监听-接收/链接

  1. 由于TCP是面向链接的套接字,所以需要服务端和客户端建立链接关系
  2. 对于服务端来说,服务端是会被多个客户端进行链接,由此服务端需要保持等待链接的状态并进行接收链接,等待客户端的链接,这样才能保证链接之后数据传输的可靠性
  3. 对于客户端来说,客户端是向服务端进行索取服务的一方,即决定链接的发起
  • listen函数原型:
代码语言:javascript复制
int listen(int sockfd, int backlog);
  • 解释:

  1. 功能:设置套接字为监听状态,即服务器时刻注意是否有客户端发来连接请求
  2. sockfd:需要设置为监听状态的套接字对应的文件描述符
  3. backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可
  4. 返回值:监听成功返回0,监听失败返回-1,同时错误码会被设置
  • accept的函数原型:
代码语言:javascript复制
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 解释:

  1. sockfd:特定的监听套接字,表示从该监听套接字中获取连接
  2. addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
  3. addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数
  4. 返回值:获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置
  • 套接字文件之间的区别:

  1. socket函数创建的套接字文件:用于不断获取客户端发来的连接请求,即进行监听获取链接
  2. accept函数创建的套接字文件:为本次accept成功获取到的连接提供网络通信服务
  • 示例:服务端
代码语言:javascript复制
    //3.建立监听-允许client进行链接server
    if(listen(listen_sock,backlog)<0)
    {
        cerr<<"listen"<<endl;
        exit(3);
    }
    //和client建立连接关系
    struct sockaddr_in peer;//获取客户端的信息
    socklen_t len=sizeof(peer);
    bzero(&peer,len);
    int sock=accept(listen_sock,(struct sockaddr*)&peer,&len);//真正进行服务的网络文件
    if(sock<0)
    {
        cout<<"accept error"<<endl;
    }
  • 示例:客户端
代码语言:javascript复制
    //客户端也不用进行listen-客户端是发起链接的一方,服务端是接收链接的需要保证listen状态
    //3.发起链接
    if(connect(sock,(struct sockaddr*)&desc,sizeof(desc)) < 0)
    {
        cerr<<"connect"<<endl;
    }

4、数据发送和接收

  1. TCP是流式套接字,同文件的读写是一样是流式的,那么对于TCP来说,使用文件读写的方式进行读写套接字文件同样可以达到数据发送和接收的目的
  2. 读取套接字文件数据,即为接收对应套接字建立链接的远端发送来的消息;向套接字文件进行写入数据,即为向对应套接字建立链接的远端发送数据
  • read函数原型:
代码语言:javascript复制
ssize_t read(int fd, void *buf, size_t count);
  • 解释:

  1. fd:特定的文件描述符,表示从该文件描述符中读取数据
  2. buf:数据的存储位置,表示将读取到的数据存储到该位置
  3. count:数据的个数,表示从该文件描述符中读取数据的字节数
  4. 返回值:如果大于0,则表示本次实际读取到的字节个数;等于0,则表示对端已经把连接关闭了;小于0,则表示读取时遇到了错误

注:如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,不必再为该客户端提供服务了

  • write函数原型:
代码语言:javascript复制
ssize_t write(int fd, const void *buf, size_t count);
  • 解释:

  1. fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字
  2. buf:需要写入的数据
  3. count:需要写入数据的字节个数
  4. 返回值:写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置

除了使用文件读写函数接口进行发送和接收网络数据,还可以使用专门的数据发送和接收接口

  • send函数原型:
代码语言:javascript复制
int send(SOCKET s,const char FAR *buf ,int len ,int flags); 
  • 解释:
  1. sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
  2. buf:待写入数据的存放位置
  3. len:期望写入数据的字节数
  4. flags:写入的方式,一般设置为0,表示阻塞写入
  5. 返回值:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
  • recv函数原型:
代码语言:javascript复制
int recv(SOCKET s ,char FAR * buf ,int len ,int flags);  
  • 解释:
  1. sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据
  2. buf:读取数据的存放位位置
  3. len:期望读取数据的字节数
  4. flags:读取的方式,一般设置为0,表示阻塞读取
  5. 返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置

注:因为TCP是面向链接的,每一个读写的套接字文件都已经确立了对应的链接对象,所以这里的recv和send并不用像UDP的recvfrom和sendto那样指定对端的网络信息

  • 注意:
  1. 一般来说 send(),recv()用于TCP,sendto()及recvfrom()用于UDP
  2. sendto可以在参数中指定发送的目标地址 , sendto可用于无连接的socket,send没有参数指定目标地址,所以需要socket已建立连接
  • 示例:服务端
代码语言:javascript复制
 while(true)
 {
     char buffer[1024]={0};
     //接收客户端的消息
     ssize_t s=read(sock,buffer,sizeof(buffer)-1);
     if(s>0)
     {
         buffer[s]=0;
         cout<<"client# "<<buffer<<endl;
         string message=buffer;
         if(message=="quit")
         {
             write(sock,"quit success!",strlen("quit success!"));
             break;
         }
         message ="[server_echo]";
         write(sock,message.c_str(),message.size());
     }
     else if(s==0)
     {
         cout<<"client close..."<<endl;
         break;
     }
     else
     {
         cerr<<"read error"<<endl;
         break;
     }
 }
  • 示例:客户端
代码语言:javascript复制
//4.执行逻辑
while(true)
{
    char buffer[1024]={0};
    cout<<"Please Enter#";//提示符
    fflush(stdout);
    ssize_t s=read(0,buffer,sizeof(buffer)-1);
    if(s>0)
    {
        buffer[s-1]=0;//覆盖回车键
        //向服务端传数据-tcp是流式套接字
        write(sock,buffer,strlen(buffer));
        //接收服务端发来的数据
        ssize_t size=read(sock,buffer,sizeof(buffer)-1);
        if(size>0)
        {
            buffer[size]=0;
            cout<<buffer<<endl;
        }
        else
        {
            cerr<<"server close"<<endl;
            break;
        }
    }
}

5、简单英译汉服务器

  • 套接字处理函数:
代码语言:javascript复制
#pragma once
#include "server.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <string.h>
#include <string>
#include <map>
//执行处理逻辑
std::map<std::string,std::string> dict={
    {"apple","苹果"},
    {"banana","香蕉"},
    {"hello","你好"},
};

void HandlerTranslation(int sock)
{
    cout<<"进行翻译处理: debug sock: "<<sock<<endl;
	while(true)
    {
        char buffer[1024]={0};
        //接收客户端的消息
        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            cout<<"client# "<<buffer<<endl;
            string message=buffer;
            if(message=="quit")
            {
                write(sock,"quit success!",strlen("quit success!"));
                break;
            }
            message ="[server_echo]";
            write(sock,message.c_str(),message.size());
        }
        else if(s==0)
        {
            cout<<"client close..."<<endl;
            break;
        }
        else
        {
            cerr<<"read error"<<endl;
            break;
        }
    }
}
void* Routine(void* args)
{ 
    int sock=*(int*)args;
    delete (int*)args;
    //线程分离-不用进行等待
    pthread_detach(pthread_self());
    HandlerTranslation(sock);
    close(sock);//线程自己进行关闭
    return nullptr;
}
//多线程-轻量化,健壮性不足
void HandlerSock(int sock)
{
    pthread_t tid;
    int* p=new int(sock);
    pthread_create(&tid,nullptr,Routine,p);
}
  • 服务端代码:
代码语言:javascript复制
server.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <stdlib.h>
#include <string>
using std::cout;
using std::cerr;
using std::endl;
using std::string;

const int backlog=5;

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

class TcpServer
{
private:
    uint16_t _port;//绑定的port
    int listen_sock;//监听套接字
public:
    TcpServer(uint16_t port)
        :_port(port),listen_sock(-1)
    {}
    void InitTcpServer()
    {
        //1.创建listen_socket套接字文件
        listen_sock=socket(AF_INET,SOCK_STREAM,0);//ipv4协议,socket类型,传输协议编号
        if(listen_sock < 0)
        {
            cerr<<"listen_sock"<<endl;
            exit(1);
        }
        //2.绑定port号及ip地址
        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)
        {
            cerr<<"bind"<<endl;
            exit(2);
        }

        //3.建立监听-允许client进行链接server
        if(listen(listen_sock,backlog)<0)
        {
            cerr<<"listen"<<endl;
            exit(3);
        }
    }
    void Loop(Handler handler)
    {
        //4.执行逻辑-接收链接并进行处理服务
        while(true)
        {
            //和client建立连接关系
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            bzero(&peer,len);
            int sock=accept(listen_sock,(struct sockaddr*)&peer,&len);//真正进行服务的网络文件
            if(sock<0)
            {
                cout<<"accept error"<<endl;
                continue;
            }
            //验证-输出链接client端信息
            uint16_t port=ntohs(peer.sin_port);//网络转主机序列
            string ip=inet_ntoa(peer.sin_addr);//4字节ip转点分十进制ip
            cout<<"debug: sock#"<<sock<<" peer_ip#"<<ip<<" peer_port#"<<port<<endl;
            //执行处理逻辑
            handler(sock);
        }
    }
    ~TcpServer()
    {
        if(listen_sock > 0)
        close(listen_sock);
    }
};
server.cc:
#include "server.hpp"
#include "Handler.hpp"

int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        cout<<"Usage:nt"<<"tco_server port"<<endl;
        return 1;
    }
    uint16_t port=atoi(argv[1]);
    TcpServer ser(port);
    ser.InitTcpServer();
    ser.Loop(HandlerSock);
    return 0;
}
  • 客户端代码:
代码语言:javascript复制
client.hpp:
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <cstring>
#include <stdio.h>
using std::cout;
using std::cerr;
using std::endl;
using std::string;

class TcpClient
{
private:
    string desc_ip;//要访问的目标端ip
    uint16_t desc_port;//要访问的目标端port
    int sock;//打开的网络文件
public:
    TcpClient(string ip,uint16_t port)
        :desc_ip(ip),desc_port(port),sock(-1)
    {
        //进行初始化工作
        //1.创建socket套接字文件
        sock=socket(AF_INET,SOCK_STREAM,0);//ipv4协议,socket类型,传输协议编号
        if(sock < 0)
        {
            cerr<<"sock"<<endl;
            exit(1);
        }
    }
    void start()
    {
        //2.创建socketaddr_in结构体并填入服务端的信息
        struct sockaddr_in desc;
        bzero(&desc,sizeof(desc));
        desc.sin_family=AF_INET;//传输协议
        desc.sin_port=htons(desc_port);//端口号
        desc.sin_addr.s_addr=inet_addr(desc_ip.c_str());//IP地址-点分十进制转四字节ip同时转成网络传输格式
        //对于客户端并不用进行绑定自己的端口-发送数据时会自动进行绑定

        //客户端也不用进行listen-客户端是发起链接的一方,服务端是接收链接的需要保证listen状态
        //3.建立链接关系
        if(connect(sock,(struct sockaddr*)&desc,sizeof(desc)) < 0)
        {
            cerr<<"connect"<<endl;
        }

        //4.执行逻辑
        while(true)
        {
            char buffer[1024]={0};
            cout<<"Please Enter#";//提示符
            fflush(stdout);
            ssize_t s=read(0,buffer,sizeof(buffer)-1);
            if(s>0)
            {
                buffer[s-1]=0;//覆盖回车键
                //向服务端传数据-tcp是流式套接字
                write(sock,buffer,strlen(buffer));
                //接收服务端发来的数据
                ssize_t size=read(sock,buffer,sizeof(buffer)-1);
                if(size>0)
                {
                    buffer[size]=0;
                    cout<<buffer<<endl;
                }
                else
                {
                    cerr<<"server close"<<endl;
                    break;
                }
            }
        }
    }
    ~TcpClient()
    {
        if(sock >= 0)
        close(sock);
    }
};
client.cc:
#include "tcp_client.hpp"

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        cout<<"Usage:nt"<<"tcp_client desc_ip desc_port"<<endl;
        return 1;
    }
    //写入ip和port
    uint16_t port=atoi(argv[2]);
    string ip=argv[1];
    TcpClient client(ip,port);
    client.start();
    return 0;
}
  • 运行效果:

0 人点赞