【网络通信】socket编程——TCP套接字

TCP依旧使用代码来熟悉对应的套接字,很多接口都是在udp中使用过的
所以就不会单独把他们拿出来作为标题了,只会把第一次出现的接口作为标题

文章目录

  • 服务端 tcp_server
    • tcpserver.hpp(封装)
      • 初始化 initServer
        • 1. 创建socket
        • 2. 绑定 bind
          • htons —— 主机序列转化为网络序列
        • 3.监听
          • listen ——设为 监听状态
      • 启动 Start
        • 1.获取连接,accept
          • accept
          • accept返回的文件描述符 与 socket设置成功返回的文件描述符的关系
        • 2.获取新连接成功,开始进行业务处理
    • tcpserver.cc (主函数main实现)
  • 客户端 tcp_client
    • tcpclient.cc(不封装,直接实现)
      • 1.创建套接字
      • 2.发起链接
        • inet_addr——字符串IP地址 转为 网络序列IP地址
      • 3. 链接成功
  • 具体代码实现
    • err.hpp(用于存放错误信息)
    • makefile
    • tcpServer.hpp( 服务端 封装)
    • tcpServer.cc( 服务端 主函数实现)
    • tcpClient.cc(客户端 不封装)

【网络通信】socket编程——TCP套接字_第1张图片

通过TCP的套接字 ,来把数据交付给对方的应用层,完成双方进程的通信

服务端 tcp_server

tcpserver.hpp(封装)

在 tcpServer.hpp 中,创建一个命名空间 yzq 用于封装
在命名空间中,定义一个类 TcpServer

该类中包含 构造 析构 初始化(initServer) 启动(start)


初始化 initServer

1. 创建socket

【网络通信】socket编程——TCP套接字_第2张图片

设置监听端口号(后面会解释) ,需要端口号标识进程的唯一性


【网络通信】socket编程——TCP套接字_第3张图片

在类外设置一个默认端口号8888作为构造函数参数port的缺省值


【网络通信】socket编程——TCP套接字_第4张图片

创建套接字


输入 man socket

【网络通信】socket编程——TCP套接字_第5张图片

第一个参数 domain ,用于区分 进行网络通信还是 本地通信
若想为网络通信,则使用 AF_INET
若想为本地通信,则使用 AF_UNIX

第二个参数 type, 套接字对应的服务类型


SOCK_STREAM 流式套接
SOCK_DGRAM 无连接不可靠的通信(用户数据报)

第三个参数 protocol ,表示想用那种协议,协议默认为0
若为 流式套接,则系统会认为是TCP协议 ,若为用户数据报,则系统会认为是UDP协议

套接字的返回值:若成功则返回文件描述符,若失败则返回 -1


说明进行网络通信,流式套接,同时系统认为是TCP协议


【网络通信】socket编程——TCP套接字_第6张图片

创建err.hpp 用于存储错误信息的枚举


【网络通信】socket编程——TCP套接字_第7张图片

如果创建失败,则终止程序

2. 绑定 bind

输入 man 2 bind ,查看绑定

【网络通信】socket编程——TCP套接字_第8张图片

给一个套接字绑定一个名字
第一个参数 sockfd 为 套接字
第二个参数 addr 为 通用结构体类型
第三个参数 addrlen 为 第二个参数的实际长度大小

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


使用bind,是需要借助一个通用结构体来实现的
所以定义一个 网络通信类型的结构体 local

在上一篇博客中,详细讲述了 sockaddr_in 结构体的内部组成
不懂可以去看看:struct sockaddr_in 的理解


htons —— 主机序列转化为网络序列

输入 man htons ,表示短整数的主机转网络序列

【网络通信】socket编程——TCP套接字_第9张图片

所以需要将主机的port_进行转化 ,然后再交给 local的sin_port (端口号)


【网络通信】socket编程——TCP套接字_第10张图片

INADDR_ANY 表示bind的任意IP


【网络通信】socket编程——TCP套接字_第11张图片

如果绑定失败返回-1


3.监听

listen ——设为 监听状态

输入 man 2 listen
设置当前套接字状态为 监听状态

【网络通信】socket编程——TCP套接字_第12张图片

第一个参数 sockfd 为 套接字
第二个参数 暂不做解释,一般设为整数
若成功则返回0,若失败返回-1


【网络通信】socket编程——TCP套接字_第13张图片

监听失败 返回-1,并终止程序


在类外设置一个 默认整数 为32

启动 Start

设置一个布尔变量 quit_,若为true则表示 服务器启动, 若为false,则表示 服务器没有启动


如果服务器没有启动,则进入while循环

【网络通信】socket编程——TCP套接字_第14张图片

1.获取连接,accept

accept

输入 man 2 accept

需要知道谁连的你,所以要获取到客户端的相关信息

第一个参数 sockfd 为套接字
第二个参数 addr 为通用结构体类型的 结构体 这个结构体是用来记录客户端内的port号以及IP地址 、16位地址类型等信息
第三个参数 addrlen 为 结构体的大小

返回值:
若成功,则返回一个合法的整数 即文件描述符
若失败,返回-1并且设置错误码

accept返回的文件描述符 与 socket设置成功返回的文件描述符的关系

如:有一个鱼庄,生意不太好,所以在外面站着一个人叫张三,进行揽客
有一天你和你的朋友在外面遇见张三,张三就向你们说他们鱼庄有多少,推荐去他们哪里吃鱼
正好你们俩也饿了,所以就跟张三去鱼庄吃鱼,但是只有你们进入鱼庄了,张三并没有进去
张三只是向里面喊了一声,来客人了,然后继续找人去了
这个时候来了一个服务员李四,向你们询问要吃什么,并向你们提供各种服务

每一次张三把客人招呼到鱼庄时,都会有一名服务员给客人提供服务
当张三做完自己的工作后,立马返回自己的工作岗位,继续招揽客人

张三不给用户提供具体的服务,只负责把客人从路上拉到店里去吃饭 进行消费
李四来给客人提供服务
鱼庄 可以看作是 整个服务器
像张三这样把客人从外部 拉到餐厅里的 称为 监听套接字 即accept的第一个参数 sockfd
像李四这样作的动作,相当于accept会返回一个文件描述符,这个文件描述符 是真正给用户提供IO服务的


若张三继续拉客,在路上碰见一个人,问他要不要去鱼庄吃饭,但那个人摇了摇头,表示没有意愿去鱼庄吃饭,
此时张三就被拒绝了,但这并不影响张三继续拉客去鱼庄
所以 accept 获取失败,只需继续 执行即可

【网络通信】socket编程——TCP套接字_第15张图片

2.获取新连接成功,开始进行业务处理

提供一个service的函数 ,参数为新的文件描述符sock
用于实现基本的读写服务 即 客户端发消息,需要把消息转回去


TCP 是一种流式服务
输入 man 2 read

【网络通信】socket编程——TCP套接字_第16张图片

从文件描述符fd中将我们想要的数据,按照数据块的方式读取出来
返回值代表多少字节,读取到文件结尾为0,失败为-1


【网络通信】socket编程——TCP套接字_第17张图片

将sock中的数据读取到buffer缓冲区中
若读取成功,则将最后一位的下一位赋值为0


【网络通信】socket编程——TCP套接字_第18张图片

若read的返回值为0,则对方将连接关闭了,所以sock也可以关闭


【网络通信】socket编程——TCP套接字_第19张图片

若返回值小于0,则读取失败,返回错误码


收到消息,需要把消息做某种处理后,再把消息转回去
所以使用 包装器 functional处理

在类外设置一个函数类型,返回值为string,参数为 string 的包装器


【网络通信】socket编程——TCP套接字_第20张图片

用该函数类型定义为一个私有变量func


【网络通信】socket编程——TCP套接字_第21张图片

将处理完的消息进行返回
输入 man 2 write
向一个文件中写入信息

【网络通信】socket编程——TCP套接字_第22张图片

fd代表文件描述符
buf代表 缓冲区
count代表 缓冲区大小
write将缓冲区的count大小的数据写入 fd中


【网络通信】socket编程——TCP套接字_第23张图片

将res中的数据 写入 sock文件描述符中

tcpserver.cc (主函数main实现)

想要只输入 ./tcp_server 加 端口号
所以在main函数中添加命令行参数
main函数的两个参数,char* argv[] 为指针数组 ,argv为一张表,包含一个个指针,指针指向字符串
int argc,argc为数组的元素个数


【网络通信】socket编程——TCP套接字_第24张图片

当参数输入不为2时,就会终止程序,同时打印出对应的输入参数


【网络通信】socket编程——TCP套接字_第25张图片

通过构造函数了解, 想要使用 new TcpServer 需要传入回调和端口号


【网络通信】socket编程——TCP套接字_第26张图片

客户端 tcp_client

tcpclient.cc(不封装,直接实现)

为了使用客户端,所以要输入对应的 可执行程序 serverip serverport
所以在main函数需要使用 命令行参数


【网络通信】socket编程——TCP套接字_第27张图片

若输入的参数少于3个,则终止程序,并打印出对应输入的参数


将输入的第二个参数的IP地址 赋值给 serverip
将输入的第三个参数的端口号,使用atoi将字符串转化为整数 ,再赋值给serverport

1.创建套接字

【网络通信】socket编程——TCP套接字_第28张图片

网络通信,并为流式套接,默认为0,因为流式所以为TCP协议
若创建套接字失败,则终止程序


2.发起链接

输入 man accept

【网络通信】socket编程——TCP套接字_第29张图片

客户端 通过套接字sockfd,向特定的服务器发起链接请求
sockfd:套接字
addr:公共类型的结构体 内部包含 服务器的IP地址和的端口号
addrlen:结构体的大小

返回值:若成功,则返回0,若失败,返回-1和错误码
首次发起链接时,操作系统会给客户端自动进行绑定端口


所以需要先定义一个结构体server

【网络通信】socket编程——TCP套接字_第30张图片

借助htons 将上述的主机序列端口号serverport 转化为网络序列端口号

inet_addr——字符串IP地址 转为 网络序列IP地址

输入man inet_addr

【网络通信】socket编程——TCP套接字_第31张图片

第一个参数为 字符串风格的IP地址
第二个参数 为 网络序列的IP地址
将 字符串风格的IP地址 转为 网络序列的IP地址


再将主机序列的IP地址serverip,转化为网络序列的IP地址


【网络通信】socket编程——TCP套接字_第32张图片

cnt表示重连次数
设置while循环,当不等于0链接失败时,cnt值减1,并重新链接,若cnt值为0,则break终止循环
若出了while循环,cont小于等于0,则终止程序


3. 链接成功

【网络通信】socket编程——TCP套接字_第33张图片

创建一个string类型的line,将输入的参数传入line中
使用write,将line的内容传入文件描述符中
使用read,将sock的数据传入buffer中
通过read的返回值来判断,若返回值大于0则,输出其中内容
若返回值等于0,则说明链接关闭,则退出while循环
若返回值小于,则说明创建失败,返回错误码

具体代码实现

err.hpp(用于存放错误信息)

#pragma once 

enum
{
  USAGE_ERR=1,
  SOCKET_ERR,//2
  BIND_ERR,//3
  LISTEN_ERR//4
};


makefile

.PHONY:all
all: tcp_client tcp_server

tcp_client:tcpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f tcp_client tcp_server


tcpServer.hpp( 服务端 封装)

#pragma once

#include
#include
#include
#include
#include"err.hpp"
#include           
#include 
#include
#include
#include



namespace yzq
{
    
    static uint16_t defaultport=8888;//默认端口号
    static const int backlog=32;//默认整数为32
    using func_t=std::function<std::string(const std::string&)>;

    class TcpServer;
    class ThreadData//该类用于存放客户端的IP port 套接字
    { 
        public:
        ThreadData(int fd,const std::string&ip,const uint16_t &port,TcpServer*ts)//构造
        :sock(fd),clientip(ip),clientport(port),current(ts)
        {}
      public:
       int sock;//套接字
       std::string clientip;//客户端IP
       uint16_t clientport;//客户端端口号
       TcpServer*current;
    };
    
   class TcpServer
   {
    public:
    TcpServer(func_t func,uint16_t port=defaultport)
    :func_(func),port_(port),quit_(true)//表示默认启动
    {}
    void initServer()//初始化
    {
        //1.创建socket
        listensock_=socket(AF_INET,SOCK_STREAM,0);
        if(listensock_<0)//创建失败
        {
            std::cout<<" create socket errno"<<std::endl;
            exit(SOCKET_ERR);//终止程序
        }
        //2. bind 绑定
        struct sockaddr_in local;//网络通信类型
        //清空
        memset(&local,'\0',sizeof(local));
        local.sin_family=AF_INET;//网络通信
        //htons 主机转网络
        local.sin_port=htons(port_);//端口号
        local.sin_addr.s_addr=INADDR_ANY ; //IP地址

        if(bind(listensock_,(struct sockaddr*)&local,sizeof(local))<0)
        //失败返回-1
        {
           std::cout<<" bind socket errno"<<std::endl;
            exit(BIND_ERR);//终止程序
        }

        // 3.监听
        if(listen(listensock_,backlog)<0)
        {
            //监听失败返回-1
             std::cout<<" listen socket errno"<<std::endl;
            exit(LISTEN_ERR);//终止程序
        }
       
    }
    void start()//启动
    { 
      quit_=false;//服务器没有启动
      while(!quit_)
      {
        //4.获取连接,accept
        struct sockaddr_in client;//网络通信类型
        socklen_t len=sizeof(client);//结构体大小
        int sock=accept(listensock_,(struct sockaddr*)&client,&len);    
        if(sock<0)
        {
            //获取失败
            std::cout<<" accept  errno"<<std::endl;
            continue;//继续执行
        }
        //提取客户端信息
        std::string clientip=inet_ntoa(client.sin_addr);//客户端ip
        uint16_t clientport=ntohs(client.sin_port);//客户端端口号
        //5.获取新连接成功,开始进行业务处理
        std::cout<<"获取新连接成功: "<<sock<<"from "<<listensock_<<std::endl; 
        //service(sock);//多线程版本没有调用函数

       //多线程版本
         pthread_t tid;
         ThreadData*td=new ThreadData(sock,clientip,clientport,this);
         pthread_create(&tid,nullptr,threadRoutine,td);
      }
    }
   static void *threadRoutine(void*args)
    {   
        pthread_detach(pthread_self());//线程分离
        ThreadData*td=(ThreadData*)args;
        td->current->service(td->sock);
        delete td;
        return nullptr;
    }
    void service(int sock)
    {
        char buffer[1024];
        while(true)
        {
            //将sock中的数据读取到buffer中
            ssize_t s=read(sock,buffer,sizeof(buffer)-1);
             if(s>0)
             {
                //读取成功
                buffer[s]=0;
                //使用func 进行回调
                std::string res=func_(buffer);
                std::cout<<res<<std::endl;
                //将res中的数据写给sock中
                write(sock,res.c_str(),res.size());

             }
             else if(s==0)
             {
                //说明对方将连接关闭了
                close(sock);
                std::cout<<"client quit,me too"<<std::endl;
                break; 
             }
             else 
             {
                //读取失败返回-1
                std::cout<<"read errno"<<strerror(errno)<<std::endl;
                break;
             }
        } 
    }
    ~TcpServer()
    {}
      private:
      func_t func_;//函数类型
      int listensock_;//监听套接字 
      bool quit_;//表示服务器是否启动
      uint16_t port_;//端口号
   };
}


tcpServer.cc( 服务端 主函数实现)

#include"tcpServer.hpp"
#include//智能指针
using namespace std;
using namespace yzq;

static void usage(string proc)
{
    std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}

std::string echo(const std::string&message)
{
    return message;
}
// ./tcp_server port
int main(int argc,char*argv[])
{
    //输入两个参数 所以不等于2
    if(argc!=2)
    {
       usage(argv[0]);
       exit(USAGE_ERR);//终止程序
    }
    //将输入的端口号 转化为整数 
    uint16_t port=atoi(argv[1]);
   unique_ptr<TcpServer>tsvr(new TcpServer(echo,port));
   tsvr->initServer();//服务器初始化
   tsvr->start();//启动
    return 0;
}

tcpClient.cc(客户端 不封装)

#include
#include
#include
#include           
#include 
#include
#include
#include"err.hpp"
using namespace std;

static void usage(string proc)
{
    std::cout<<"usage:\n\t"<<proc<<"port\n"<<std::endl;
}

//./tcp_client  serverip serverport
int main(int argc,char*argv[])
{
   if(argc!=3)
   {
       usage(argv[0]);
       exit(USAGE_ERR);//终止程序
   }
   std::string serverip=argv[1];//IP地址
   uint16_t serverport=atoi(argv[2]);//端口号
   
   //1.创建套接字
   int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
      //创建失败
      cout<<"socket errnr:"<<strerror(errno)<<endl;
      exit(SOCKET_ERR);//终止程序
    }

    //2.发起链接
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));//清空
    server.sin_family=AF_INET;//网络通信类型
    //htons 主机序列转为网络序列
    server.sin_port=htons(serverport);//网络端口号
    inet_aton(serverip.c_str(),&server.sin_addr);//网络IP地址

    int cnt=5;//重连次数
    while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)
    {
      //不等于0则链接失败
      sleep(1);
      cout<<"正在尝试重连,重连次数还有:"<<cnt--<<endl;
      if(cnt<=0)
      {
        //没有重连次数
        break;
      }
    }
    if(cnt<=0)
    {
        //链接失败
        cout<<"链接失败.."<<endl;
        exit(SOCKET_ERR);//终止程序
    }

    char buffer[1024];
    //3.链接成功
    while(true)
    {
        string line;
        cout<<"enter>>";
        getline(cin,line);//从cin中获取内容 写入line中
        write(sock,line.c_str(),line.size());//将line中的内容写入到sock文件描述符中   
        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            cout<<"server echo"<<buffer<<endl;
        }
        else if(s==0)
        {
            cout<<"server quit"<<endl;
            break;
        }
        else 
        {
            cout<<"read errno"<<strerror(errno)<<endl;
            break;
        }
    }
    close(sock);
    return 0;
}

你可能感兴趣的:(计算机网络,tcp/ip,网络,网络协议)