Linux网络:Socket套接字编程 | TCP

文章目录

  • 函数指针类型
  • 流套接字TCP协议编程
    • 创建绑定
    • 监听、接受 | 发起连接
    • 数据的收发
    • TCP服务端
    • 回调函数:处理通信数据 | 简易的网络翻译
    • TCP客户端
    • 查看UDP | TCP 进程服务
  • 三次握手、四次挥手

全文约 10031 字,预计阅读时长: 29分钟


函数指针类型

  • 通过typedef定义函数指针类型,通过函数指针类型定义函数指针。
  • 函数指针和函数指针类型
---//tcp_server.hpp
#include 
namespace ns_tcp_server
{
    typedef void (*handler_t)(int);
    void hand_add(int x)
    {
        ++x;
        std::cout<<x<<std::endl;
    }

    void hand_sub(int x)
    {
        --x;
        std::cout<<x<<std::endl;
    }
    void loop(handler_t hand)
    {
        int a = 25;
        hand(a);
    }
}
---//main.cc
#include "tcp_server.hpp"
using namespace ns_tcp_server;

int main()
{
    loop(hand_sub);
    //loop(hand_sub);
    return 0;
}
  • Windows与Linux云服务器之间传文件:首先,服务器要安装了rz,sz;yum install lrzsz;
  • 运行rz,会将windows的文件传到linux服务器;运行sz filename,会将文件下载到windows本地。

流套接字TCP协议编程

  TCP 相比 UDP,流套接字(SOCK_STREAM)用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。

  TCP服务端:首先需要创建一个监听套接字文件;接下来绑定IP地址和端口号到监听套接字文件;然后进行监听;最后获取连接,进行数据的收发和处理。

  TCP客户端:首先创建套接字;不需要绑定;接着请求连接;然后完成数据的收发。
Linux网络:Socket套接字编程 | TCP_第1张图片


创建绑定

先创建的套接字一般叫做:监听套接字。相当于迎宾人员,真正进行通信的是获取连接后返回的套接字文件。

  • int socket(int domain, int type, int protocol);,socket() 打开一个网络通讯端口,如果成功的话,就像 open ()一样返回一个文件描述符;
    • domain :对于IPv4, family参数指定为 AF_INET;
    • type:对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议
#include          
#include 
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock < 0)
{
    std::cout << "socket error" << std::endl;
    exit(2);
}

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

  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);,bind() 成功返回0,失败返回-1。
    • sockfd:上一步创建的返回值。
    • const struct sockaddr *addr:IPV4 就使用 struct sockaddr_in;传参时强转成 struct sockaddr*
      • struct sockaddr_in.sin_addr.s_addr,是用来绑定IP地址的,参数推荐设置成:INADDR_ANY
    • socklen_t addrlen:上一个结构体的大小。
  • 网络地址为INADDR_ANY ,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址,,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个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)
{
   std::cerr << "bind error" << std::endl;
   exit(3);
}

监听、接受 | 发起连接

listen()声明sockfd处于监听状态,,并且最多允许有backlog个客户端处于连接等待状态,,如果接收到更多的连接请求就忽略,,这里设置不会太大(一般是5)。

  • int listen(int sockfd, int backlog);,listen()成功返回0,失败返回-1。
const int backlog =  5;
if(listen(listen_sock,backlog)<0)
{
   cerr << "listen error" <<  endl;
   exit(3);
}

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

  • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);成功返回一个新的用于通信的套接字文件描述符;失败返回-1。
    • sockfd:创建的监听套接字
    • 后面两个:输入输出型参数,返回时传出客户端的地址和端口号;如果给addr 参数传NULL,表示不关心客户端的地址;
while(true)
{
  struct sockaddr_in peer;
  socklen_t len = sizeof(peer);
  int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
   if(sock < 0)
   {
     std::cout << "warning: accept error" << std::endl;
     continue;
   }
}

客户端需要调用 connect() 连接服务器;connect和 bind 的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;

  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);connect()成功返回0,出错返回-1;
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;
 return;
}

数据的收发

  • 应用程序可以像读写文件一样用 read/write 在网络上收发数据。
  • 还可以使用专门的数据发送和接收接口 send()recv()
    • 用哪个网络文件写入或读取,数据从哪个缓冲区读取或写入缓冲区,读写多少个字节,0采用阻塞的方式读取
    • 返回值:入成功返回实际写入或读取的的字节数,写入失败返回-1,同时错误码会被设置。
int send(SOCKET s,const char FAR *buf ,int len ,int flags); 
int recv(SOCKET s ,char FAR * buf ,int len ,int flags); 


TCP服务端

  • 使用类进行了封装,sv.hpp头文件
#pragma once 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using  std::cout;
using  std::endl;
using  std::cin;
using  std::cerr;
using  std::string;

namespace ns_tcp_server
{
    typedef void (*handler_t)(int);
    
    const int backlog =  5;

    class TcpSe
    {
        private:
            uint16_t _port;
            int listen_sock;
        public:
            TcpSe(uint16_t port)
                :_port(port)
                 ,listen_sock(-1)
        {}
        void InitSe()
        {
            listen_sock = socket(AF_INET,SOCK_STREAM,0);
            if(listen_sock<0)
            {
                 cerr << "socket error" << endl;
                exit(1);
            }
            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 error" <<  endl;
                exit(2);
            }
            //监听 正式传递数据之前,要先建立连接
            if(listen(listen_sock,backlog)<0)
            {
                 cerr << "listen error" <<  endl;
                exit(3);
            }
        }
        //循环获取连接,通信,处理数据
        void Loop(handler_t hand)
        {
            while(true)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
                if(sock < 0)
                {
                     cout<< "warning : accept error" <<  endl;
                    continue;
                }
                
                uint16_t peer_port = ntohs(peer.sin_port);
                 string peer_ip = inet_ntoa(peer.sin_addr);//IP风格转换
                 cout << " debug: ip : "<< peer_ip <<" port: "<< peer_port<< endl;
                
                hand(sock);
                
                close(sock);//线程版本应该,让线程关闭
            }
        }
        ~TcpSe()
        {
            if(listen_sock >= 0)
            {
                close(listen_sock);
            }
        }
    };
}

  • 服务端sv.cc
#include "hd.hpp"             
#include "sv.hpp"     

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

//.server port
int main(int argc,char* argv[])
{
//    if(argc != 2)
//    {
//        Usage(argv[0]);
//        exit(1);
//    }
    uint16_t port = atoi("8081");
    ns_tcp_server::TcpSe* svr = new ns_tcp_server::TcpSe(port);
    svr->InitSe();
    std::cout << "初始化完成"<<std::endl;
    //svr->Loop(ns_handler::hand_commn);
    //svr->Loop(ns_handler::hand_multiprocess);
    //svr->Loop(ns_handler::hand_multithread);
    svr->Loop(ns_handler::hand_tpool);
    return 0;
}


回调函数:处理通信数据 | 简易的网络翻译

  • hd.hpp包含:单进程版本、多进程版本、多线程版本、线程池版本
#include "sv.hpp"
#include "tpol.hpp"
#include 
#include 
#include 
#include 

#define SIZE 1024

namespace ns_handler
{
    void hand_help(int sock)
    {
         cout << "回调函数被调用 debug: "<< sock << endl;
        while(true)
        {
            char buff[SIZE];
            ssize_t s = read(sock,buff,sizeof(buff)-1);
            if(s>0)
            {
                buff[s]=0;
                cout<< "client: "<<buff << endl;
                string echo_message = buff;
                if(echo_message == "quit")
                {
                    break;
                }
                echo_message += "server says";
              // cout<
               send(sock,echo_message.c_str(),echo_message.size(),0);
                //write(sock,echo_message.c_str(),echo_message.size());
            }
            else if (s==0)
            {
                 cout <<sock << ": client quit----"<< endl;
                break;
            }
            else{
                 cerr <<"read error"<< endl;
                break;
            }
        }
    }
    void hand_commn(int sock)
    {
        hand_help(sock);
    }
    
    //让孙子进程去执行,主进程继续去获取连接;孙子进程继承父进程的fd,用完关闭即可。
    void hand_multiprocess(int sock)
    {
        if(fork() == 0)
        {
            //child
            if(fork()>0)
            {
                exit(0);
            }
            //grandson 孤儿进程 被os领养
            hand_help(sock);
            exit(0);
        }
        waitpid(-1,nullptr,0);
    }
    void* routine(void* args)
    {
        int sock = *(int*)args;
        delete (int*)args;

        cout<< "线程_sock:"<<sock<<endl;
        pthread_detach(pthread_self());

        hand_help(sock);
        close(sock);
        return nullptr;
    }

    void hand_multithread(int sock)
    {
        cout<<"sock: "<<sock<<endl;
        pthread_t tid;
        int* p = new int(sock);
        pthread_create(&tid,nullptr,routine,p);
    }
    //线程池版本
    class task
    {
        private:
            int sock;
        public:
            task(){}
            task(int sk):sock(sk){}
            void operator()()
            {
                cout<<"当前处理客户数据的线程是"<<pthread_self()<<endl;
                hand_help(sock);
                close(sock);
            }
            ~task(){}
    };
    
    void hand_tpool(int sock)
    {
        ThreadPool<task>::Get_inst(5)->pusht(task(sock));
    }
}





  • 线程池:tpol.hpp
#pragma once 
#include 
#include 
#include 
using std::queue;

template<class T>
class ThreadPool
{
    private:
        queue<T> q;
        pthread_mutex_t lock;
        pthread_cond_t cond;
    public:
        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>* inst;
    public:
        static ThreadPool<T>* Get_inst(int num)
        {
            static pthread_mutex_t mtx  = PTHREAD_MUTEX_INITIALIZER;
            if(nullptr == inst)
            {
                pthread_mutex_lock(&mtx);
                if(nullptr == inst)
                {
                    inst = new ThreadPool<T>();
                    inst->InTp(num);//线程池创建 num个 线程 申请及创建
                }
                pthread_mutex_unlock(&mtx);
            }
            return inst;
        }
        static void* Run(void* args)//类的内部所以要设置成静态的
        {//线程执行函数只能由一个参数
            pthread_detach(pthread_self());
            ThreadPool* tp = (ThreadPool*)args;
            while(true)
            {
                //上锁,线程间互斥,保证数据的安全一致性。
                //条件变量,避免线程饥饿,保持线程间同步
                //通过接口使用this属性的方法,美观环保。
                tp->Lkq();
                while(tp->Ispty())//while排除伪唤醒
                {//任务队列没任务就阻塞等待。
                    tp->cnwait();
                }
                //从队列里取任务。
                T t;
                tp->popt(&t);
                tp->ulkq();
                t();//处理任务
            }
        }
        void Lkq()
        {
            pthread_mutex_lock(&lock);
        }
        void ulkq()
        {
            pthread_mutex_unlock(&lock);
        }
        void cnwait()
        {
            pthread_cond_wait(&cond,&lock);
        }
        void cnwake()
        {
            pthread_cond_signal(&cond);
        }
        bool Ispty()
        {
            return q.size() == 0;
        }
        void popt(T* out)
        {
           *out= q.front();
           q.pop();
        }
        void InTp(int num)//创建n个线程
        {
            for(auto i = 0; i < num;i++)
            {
                pthread_t tid;
                pthread_create(&tid,nullptr,Run,this);
                //传this 使用类中的属性以及方法
            }
        }
        void pusht(const T& in)
        {
            //放任务,要么没放,要么放了,加锁保证数据的原子性
            //然后唤醒消费者取任务
            Lkq();
            q.push(in);
            cnwake();
            ulkq();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        }
};


TCP客户端

  • cl.hpp
#pragma once 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using std::cout;
using std::cin;
using std::string;
using std::cerr;

namespace ns_client
{
    class Tcp_client
    {
        private://需要服务器的IP和端口
             string desc_ip;
            uint16_t desc_port;
            int sock;
        public: 
            Tcp_client( string _ip,uint16_t _port)
                :desc_ip(_ip)
                 ,desc_port(_port)
            {}
            void Init_Client()
            {
                //sock
                sock = socket(AF_INET,SOCK_STREAM,0);
                if(sock<0)
                {
                     cerr << "socket error"<<std::endl;
                    exit(1);
                }
                //不需要绑定 不需要监听 不需要accept
            }
            //通信之前 需要先建立连接
            void Start()
            {
                //填充对方的socket信息
                struct sockaddr_in svr;
                bzero(&svr,sizeof(svr));
                svr.sin_family = AF_INET;
                svr.sin_addr.s_addr = inet_addr(desc_ip.c_str());
                svr.sin_port = htons(desc_port);

                //2.发起连接请求
                if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))==0)
                {
                     cout<< "connect succes"<<std::endl;
                }
                else{
                     cout<< "connect failed"<<std::endl;
                    return;
                }
                
                //3.完成业务逻辑
                while(true)
                {
                    char buff[1024]={0};
                     cout<<sock<<"please enter ";
                    fflush(stdout);
                        
                    ssize_t s = read(0,buff,sizeof(buff)-1);
                    if(s>0)
                    {
                        buff[s-1]=0;
                        write(sock,buff,strlen(buff));
                       int rs =  recv(sock,buff,sizeof(buff)-1,0);
                       if(rs>0){
                       buff[rs]=0;
                       cout<<buff<<std::endl;
                       }
                       else{
                           break;
                       }
//                        ssize_t rs = read(sock,buff,sizeof(buff)-1);
//                        if(rs>0)
//                        {
//                            buff[s]=0;
//                             cout<
//                        }
//                        else{
//                             cout<<"server close---"<
//                            break;
//                        }
                    }
                }
            }
            ~Tcp_client()
            {
                if(sock>=0)
                {
                    close(sock);
                }
            }
    };
}

  • cl.cc
#include "cl.hpp"
#include 

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

int main(int argc,char* argv[])
{
   // if(argc!=3)
   // {
   //     Usage(argv[0]);
   //     return 1;
   // }
     string ip = "127.0.0.1";
    uint16_t port = atoi("8081");

    ns_client::Tcp_client cli(ip,port);
    cli.Init_Client();
    cli.Start();
    return 0;
}


查看UDP | TCP 进程服务

  • netstat -ntlp
  • 测试:
  • 写好的服务端代码,还没写客户端时,可以用命令:telnet ip 端口号,进行测试;再按Ctrl ],即可进行通信;再按一次,输入 quit 退出。
  • Linux下安装telnet

三次握手、四次挥手

  客户端的connect触发三次握手,底层由OS自动完成,建立连接;服务端的accept()是三次握手完成后, 服务器调用 accept() 接受连接。由于TCP协议是全双工通信;所以两端都要关闭文件描述符,这就称之为四次挥手。服务端、客户端各自的close 执行挥手中的两次,也是有OS自动完成的。全双工通信:任何时刻,我可以给你发数据,你也可以给我发;半双工通信:任何时刻只能单向的发送数据。

建立连接的本质是,双方的操作系统,构建相应的内核数据结构(结构体),后续维护连接;因此建立连接也是有时间、空间成本的。计算机的名词对应着OS中的数据结构。

  • TCP通信接收数据不完整的解决方法 | 以及双端通信流程

你可能感兴趣的:(Linux,linux,tcp/ip,服务器,开发语言,后端)