socket网络编程套接字TCP/UDP两种方式详解

目录

    • 准备知识
      • 源IP地址和目的IP地址
      • 端口号与进程ID
      • 传输层协议--TCP
      • 传输层协议--UCP
      • 网络字节序
    • socket套接字介绍
      • 概念
      • 常见的三种socket
      • socket编程常见API
      • sockaddr结构
    • socket编程应用
      • 基于UDP协议的客户端/服务端
      • 服务端udp_server:
      • 客户端udp_client:
    • 改造版本,利用udp模拟minishell
      • udp_server
      • udp_client
    • 基于TCP协议的客户端/服务端
      • 服务端server.hpp:
      • 服务端server.cc:
      • 客户端client.cc:
      • 服务器数据处理方式handler.hpp:
      • 效果展示
    • 关于多版本的回调函数以及多进程多线程处理方式
      • handler.hpp
      • 浅了解TCP三次握手和四次挥手
      • TCP与UDP的对比
      • 拓展:

准备知识

源IP地址和目的IP地址

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

假如A计算机向B计算机需要通过网络发送数据,那么A的ip地址就是源IP地址,B的ip地址时目的IP地址

仅有ip地址只能满足A计算机找得到B计算机,但是A计算机发送的消息要发往B计算机哪个程序(进程)上呢?

发消息的目的就是为了让B处理,B处理当然需要一个进程来操作;

端口号与进程ID

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

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

一个端口号只能绑定唯一的进程,而一个进程可以绑定多个端口号;

一个进程的端口号相当于该进程在所处电脑内的编号,用于网络通信方便定位;操作系统分配的进程ID具有同样的定位进程效果,但是应用场景的不同,促生了两套看起来类似但又不矛盾的方式;

传输层协议–TCP

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

传输层协议–UCP

  • 传输层协议
  • 无连接
  • 不可靠传输du
  • 面向数据报

网络字节序

通过对内存的研究得知,一个多字节数据eg:int 0x1234在内存或磁盘中的存储形式有大端和小端之分,

低地址->高地址

大端:12 34

小端:34 12

网络数据同样也有两者之分,规定网络通信时一律按大端字节序传送和解析,这也就叫网络字节序;

socket网络编程套接字TCP/UDP两种方式详解_第1张图片

这些函数就是为了使网络程序有可移植性,在网络传输时调用,则可以保证不管原系统内时大端还是小端储存,都按照网络字节序(大端)将网络数据发出;

h代表主机,n代表网络;其中16位代表两字节端口号(065535),32位代表四字节ip(0255.0255.0255.0~255);、

eg:htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

socket套接字介绍

概念

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
socket网络编程套接字TCP/UDP两种方式详解_第2张图片

常见的三种socket

socket主要有以下三种类型:

  1. 数据报套接字(SOCK_DGRAM)
    数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。

  2. 流套接字(SOCK_STREAM)
    流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。

  3. 原始套接字(SOCK_RAM)
    原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。

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);

socket网络编程套接字TCP/UDP两种方式详解_第3张图片

sockaddr结构

常见接口中有sockaddr结构类型的参数,其相当于一个"父类",一半用子类的指针向其传参;

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16

位端口号和32位IP地址,这也是我们后面写udp tcp通信用的结构体.

  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

socket网络编程套接字TCP/UDP两种方式详解_第4张图片
socket网络编程套接字TCP/UDP两种方式详解_第5张图片

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in;

这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

socket编程应用

基于UDP协议的客户端/服务端

socket网络编程套接字TCP/UDP两种方式详解_第6张图片

服务端udp_server:

#include
#include
#include
#include
#include
#include
using namespace std;

int main(int argc,char* argv[])//服务器启动时命令行读入参数(端口号和ip)用于和socket绑定;
{
    //创建socket套接字;这里的sock相当于打开了一个文件描述符fd,linux下一切皆文件;
    int sock = socket(AF_INET,SOCK_DGRAM,0); 
    if(sock==0)
    {
        cerr<<"socket create error"<<endl;
        return 2;
    }
    sockaddr_in local;
    memset(&local,0,sizeof(local));//清空;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));//这个端口号通过argv参数命令从shell获取
    local.sin_addr.s_addr = htonl(INADDR_ANY);//云服务器比较特殊ip建议绑定这个;
	
    //绑定端口号和ip 确定具体某台主机上的某个进程,描述服务器端的ip和端口号
    if(bind(sock,(sockaddr*)&local,sizeof(local))<0)
    {
       cerr<<"bind error"<<endl;
       return 3;
    }
    //服务器处理业务部分,挂起不间断提供服务;
   char message [1024];//缓冲区
   while(true)
   {
        //socket创建完毕,服务端已启动,接下来就循环的收数据,处理,发回;
        cout<<"服务端已启动...."<<endl;
       memset(message,0,sizeof(message));
       
        sockaddr_in peer;//peer代表远端
        socklen_t len = sizeof(peer);
        size_t s = recvfrom(sock,message,sizeof(message)-1,0,(sockaddr*)&peer,&len); //服务端从客户端收数据
        if(s>0){
        //服务端处理收到的数据;
            message[s] = 0;
            cout<<"client sent : "<<message<<endl;
            string tmp = message;
            tmp+="server had operator";
          size_t c =  sendto(sock,tmp.c_str(),tmp.size(),0,(sockaddr*)&peer,len);//服务端处理数据后发回客户端;
          if(c<0) cerr<<"发送失败"<<endl;
        }
        else{
            cerr<<"收取失败!"<<endl;
            return 4;
        }
        

   }

    close(sock);//socket本质也是一个文件描述符,那么结束服务了就关掉;
    return 0;
}

客户端udp_client:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

//前期创建sock 绑定目的ip,端口号工作与服务端类似;
int main(int argc,char *argv[])
{
    int sock = socket(AF_INET,SOCK_DGRAM,0);

    sockaddr_in desc;//DESC代表目标主机
    memset(&desc,0,sizeof(desc));
    desc.sin_family = AF_INET;
    desc.sin_port = htons(atoi(argv[2]));
    desc.sin_addr.s_addr = ntonl(inet_addr(argv[1]);//inet_addr()函数将shell键入的字符串形式的ip(点分十进制)直接转化为网络ip; 同样 inet_ntoa是将四字节网络ip转换成点分十进制得字符串ip

    //client不需要我们自己去创建sockaddr_in 并且 bind,实际上在sendto的时候,操作系统会自动随机给client bind端口号,ip,服务器接收后也能通过这个传回数据给客户端;(client又没人主动找他,所以不用显示bind一个特定的sock与sockaddr_in )
                                 
    char message[128];//缓冲区;
    while(true){
        message[0] = 0;//清空;
        int byte = read(0,message,sizeof(message));//从标准输入读数据;
        if(byte>0){
            message[byte] = 0;//手动上’/0‘
            int c = sendto(sock,message,strlen(message),0,(sockaddr*)&desc,sizeof(desc));
            if(c<-1) cerr<<"发送失败"<<endl;
            
            sockaddr_in peer;//这个peer本质其实和desc一样,都是来自服务端;
            socklen_t len = sizeof(peer);
            size_t s = recvfrom(sock,message,sizeof(message),0,(sockaddr*)&peer,&len);
            if(s>0){
                message[s] = 0;//手动添加'\0';
                cout<<"服务端处理后返回数据:"<<message<<endl;
            }
            else{
                cerr<<"接收失败"<<endl;
            }
        }
        else
        {
            cerr<<"读取失败!"<<endl;
        }
    }
    return 0;
}

socket网络编程套接字TCP/UDP两种方式详解_第7张图片

当左侧server端启动后,通过右侧client端键入数据,通过ip/端口号找到服务端,发送数据让server收到并处理以后返回给了client端;

形成一套完整的客户端发送请求,服务端处理并返回,客户端接收结果的流程;

改造版本,利用udp模拟minishell

udp_server

#include
#include
#include
#include
#include
#include
#include
using namespace std;

int main(int argc, char* argv[])//服务器启动时命令行读入参数(端口号和ip)用于和socket绑定;
{
    //创建socket套接字;这里的sock相当于打开了一个文件描述符fd,linux下一切皆文件;
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock == 0)
    {
        cerr << "socket create error" << endl;
        return 2;
    }
    //绑定端口/ip;
    sockaddr_in local;
    memset(&local, 0, sizeof(local));//清空;
    local.sin_family = AF_INET;
    local.sin_port = htons(atoi(argv[1]));//这个端口号通过argv参数命令从shell获取
    local.sin_addr.s_addr = htonl(INADDR_ANY);//云服务器比较特殊ip建议绑定这个;

    if (bind(sock, (sockaddr*)&local, sizeof(local)) < 0)
    {
        cerr << "bind error" << endl;
        return 3;
    }
    //服务器处理业务部分,挂起不间断提供服务;
    char buffer[1024];//缓冲区
    while (true)
    {

        cout << "服务端已启动端口号:" << argv[1] << endl;
        //收数据,处理,发回;
        memset(buffer, 0, sizeof(buffer));
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        size_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len); //服务端从客户端收数据
        if (s > 0) {
            //收取数据成功,进行处理;
            buffer[s] = 0;
            cout << "client code : " << buffer << endl;
    
            FILE* f = popen(buffer, "r");
            if (!f) {
                continue;
            }
		   buffer[0] = 0;//清空缓冲区;下面再利用
            string s;
           // while (fgets(buffer, 1024, f)) s += buffer;遇到/n之类会停,不方便录入
            fread(buffer,1,1024,f);
            s+=buffer;
            if (s.size() == 0)
            {
                s = "commend error!";
            }
                int c = sendto(sock, s.c_str(), s.size(), 0, (sockaddr*)&peer, len);
                if(c>0) cout<<"date has sented"<<endl;
                else cout<<"sent error"<<endl;

        }
        else {
            cerr << "收取失败!" << endl;
            return 4;
        }


    }

    close(sock);//socket本质也是一个文件描述符,那么结束服务了就关掉;
    return 0;
}

udp_client

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

//前期创建sock 绑定目的ip,端口号工作与服务端类似;
int main(int argc,char *argv[])
{
    int sock = socket(AF_INET,SOCK_DGRAM,0);

    sockaddr_in desc;//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]);

    char message[128];//缓冲区;
    while(true){
        cout<<"[root@minishell ~]$";//shell命令行的头信息
        fflush(stdout);//强制刷新
        message[0] = 0;//清空;
        int byte = read(0,message,sizeof(message));//从标准输入读数据;
        if(byte>0){
            message[byte-1] = 0;//手动上’/0顺便去掉键入的\n多于换行
            int c = sendto(sock,message,strlen(message),0,(sockaddr*)&desc,sizeof(desc));
            if(c<-1) 
            {
                cout<<"sent error"<<endl;
                continue;
            }           
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            size_t s = recvfrom(sock,message,sizeof(message),0,(sockaddr*)&peer,&len);
            if(s>0){
                message[s] = 0;//手动添加'\0';
                cout<<"服务端处理后返回数据:"<<message<<endl;
            }
            else{
                cerr<<"接收失败"<<endl;
            }
        }
        else
        {
            cerr<<"读取失败!"<<endl;
        }


    }
    return 0;
}

socket网络编程套接字TCP/UDP两种方式详解_第8张图片

基于TCP协议的客户端/服务端

下列三个函数是tcp相比于udp多出的三个socket编程接口;

因为TCP是有连接的,因此多了一个server进入监听,开始accept等待,client用connect主动建立连接的过程;

// 开始监听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);

socket网络编程套接字TCP/UDP两种方式详解_第9张图片

服务端server.hpp:

//进行一个简单的封装,有利于理解面向对象面程和函数回调机制;
#pragma once
#include
#include
#include
#include
#include
#include
using namespace std;

typedef void(*handler_p)(int);//函数指针handler,用于下列处理数据的回调函数;
class TcpServer
{
    private:
        int port;
        int listen_sock;
    public:
        TcpServer(int _port):port(_port),listen_sock(-1)
        {}
        void InitTcpServer()
        {
           	//创建listen_sock
            listen_sock = socket(AF_INET,SOCK_STREAM,0);
            if(listen_sock<0){
                //..error
            }
            sockaddr_in local;
            bzero(&local,sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port= htons(port);
            local.sin_addr.s_addr= INADDR_ANY;//服务器的特殊性
       		//将创建的listen_sock与对应ip和端口号绑定
            if(bind(listen_sock,(sockaddr*)&local,sizeof(local))<0){
                cerr<<"bind error"<<endl;
                exit(0);
            }
            
            //告诉系统进入监听状态,准备accept;
           listen(listen_sock,5);
           
        }
        void Loop(handler_p handler)
        {
            //循环启动服务
            while(true){
                sockaddr_in peer;
                socklen_t len = sizeof(peer);
                
                //建立好链接 获取client消息
               int sock =  accept(listen_sock,(sockaddr*)&peer,&len);//listen_sock代表服务器,如果有人连接到了正在监听listen的server服务器,那么服务器的accept会补获它,返回的int sock就是补获到的刚刚连接到他自己的peer而打开的sock文件描述符,用于与其进行数据交互;
               if(sock<0){
                   cerr<<"accept error"<<endl;
                   continue;
               }
                cout<<"连接完毕:打开fd:"<<sock<<endl;
               //回调机制,获取数据并处理;
               handler(sock);  
               close(sock);
            }
        }
        ~TcpServer()
        {
            if(listen_sock>=0) close(listen_sock);
        } 
};

服务端server.cc:

#include"server.hpp"
#include"handler.hpp"
int main(int argc,char*argv[])
{
    TcpServer tc(atoi(argv[1]));
    tc.InitTcpServer();
    tc.Loop(V1);
    return 0;
}

客户端client.cc:

#include"handler.hpp"
#include
#include
#include
using namespace std;
int main(int argc,char* argv[])
{
    //类比server的前期操作
   int sock =  socket(AF_INET,SOCK_STREAM,0);
   if(sock<0){
       //error
   }
   sockaddr_in desc;
   desc.sin_family = AF_INET;
   desc.sin_port = htons(atoi(argv[2]));
   desc.sin_addr.s_addr =inet_addr(argv[1]);
   //!建立连接!desc 为远端服务器;
   if(connect(sock,(sockaddr*)&desc,sizeof(desc))<0){
       cerr<<"connect error"<<endl;
       exit(1);
   }
   char buff[1024];
   while(true){ 
       buff[0] = 0;//清空
       cout<<"请输入数据: ";
       fflush(stdout);
       int c = read(0,buff,1024);//读入等会发送的消息
        buff[c-1] = 0;
        write(sock,buff,strlen(buff));//'\n'过滤掉了;a
        cout<<"sendto server:"<<buff<<endl;
        c = read(sock,buff,1024);
        buff[c] = 0;
        cout<<"server say:"<<buff<<endl;

   }
    return 0;
}

服务器数据处理方式handler.hpp:

其中存放各种版本的回调函数;

#pragma once
#include"server.hpp"
void handler(int sock)
{//while执行,否则一个客户端的链接只能通信一次之后就被永久挂起(connect断开了);
    while(true){
         char buff[1024];
         int c = read(sock,buff,1024);//从client收取数据
        if(c==0) break;
         buff[c-1] = '\0';//消掉'\0';
        cout<<"client say:"<<buff<<endl;
        write(sock,buff,c);//数据处理完毕后,发回client;
    }
}

void V1(int sock)//V1版本的数据处理方法;
{
    handler(sock);
}

效果展示

socket网络编程套接字TCP/UDP两种方式详解_第10张图片

关于多版本的回调函数以及多进程多线程处理方式

上述TCPserver端,如果客户端1与其连接进行数据交互,那就必须要等到客户端1与他断开连接以后,客户端2才能连接到server进行数据交互

为解决这个问题,我们引入多进程多线程版本;

handler.hpp

#pragma once
#include"server.hpp"
void handler(int sock)
{//while执行,否则一个客户端的链接只能通信一次之后就被永久挂起(connect断开了);
    while(true){
         char buff[1024];
         int c = read(sock,buff,1024);//从client收取数据
        if(c==0) break;
         buff[c-1] = '\0';//消掉'\0';
        cout<<"client say:"<<buff<<endl;
        write(sock,buff,c);//数据处理完毕后,发回client;
    }
}

void V1(int sock)//V1版本的数据处理方法(直接处理);
{
    handler(sock);
}
void V2(int sock)//(多进程版本);
{
    if(fork()==0){
        //子进程
        if(fork()==0){
            //孙子进程,执行数据处理任务;(其任务执行完没有父进程回收没事,孤儿进程会被os回收)
         	handler(sock)close (sock);
        }
        else{
            exit(0);//子进程退出,父进程就可以回收成功,接触阻塞状态进行下一个客户端的连接;
        }
    }
    waitpid(-1,nullptr,0);
    //总结:
    //为防止僵尸进程,父进程需回收子进程;
    //子进程再度fork()出孙子进程处理任务,而子进程本身直接退出,那么父进程立马可以回收成功转向下一个链接,解除阻塞;
    //孙子进程因为子进程已退出,结束时变孤儿进程被os自动回收处理;
}


void* routine(void *args)
{
    int sock = *(int*)args;
    delete (int*)args;
    handler(sock);
    pthread_detach(pthread_self());
    handler(sock);
    close(sock);//处理完业务关闭文件描述符,防止文件描述符泄露;
    return NULL;
}
void V3(int sock)//(多线程版本);
{
    pthread_t pid;
    int *psock = new int(sock);//搞堆上的原因是V1结束,局部栈帧内sock会销毁,那么创建的线程里就丢失sock这个文件了;
    pthread_create(&pid,NULL,routine,psock);
}


上述引入多进程多线程版本目的是为了实现同一个server下多个客户端的并发运行;

创建进程的代价高于线程,因此多线程版本优于多进程;

可server不停止,那么这样创建的线程是无上限的,一旦请求太多,可能出现内存不够等严重问题,因此最优方案应该引入线程池!

浅了解TCP三次握手和四次挥手

socket网络编程套接字TCP/UDP两种方式详解_第11张图片

三次握手:

  • server端进行socket();bind();listen();accept();为连接做准备;
  • client端socket();后通过connect()发起三次握手,双方OS自动进行连接,连接好后,server端的accept()拿到client的sock,之后便可以进行通信;

四次挥手:

  • 断开连接需要双方默认;TCP是全双工通信,则断开连接需要双方都断开;
  • client请求与server断开①,server作出回应,断开;一来一回算两次挥手
  • server请求与client断开②,client作出回应,断开;一来一回算两次挥手,一共四次;

TCP与UDP的对比

tcp是面向连接的,udp是无连接的。
tcp是字节流套接字,udp是数据报套接字。
tcp是可靠的,而udp是不可靠的。

各有各的优势,片面理解:tcp更可靠,但是牺牲点效率,udp不太可靠但是效率高;

拓展:

关于多线程和多进程,文件描述符sock问题;

进程的fork相当于是引用计数+1,close一个另外一个还在,只有引用计数减去到0才能关闭;
线程之间就纯粹的共享,一个关闭了就彻底关了,而不是引用计数-1;
因此,多线程和多进程版本的tcpserver,close(sock)的方式需要调整!

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