【Linux】网络套接字编程

网络编程socket

  • 预备知识
    • ▶源IP地址与目的IP地址
    • ▶端口号与进程id
    • ▶源端口号与目的端口号
    • ▶UDP与TCP协议
    • ▶网络字节序
  • 套接字(socket)介绍
    • ▶概念
    • ▶分类
    • ▶工作流程
  • socket编程
    • ▶socket编程接口
    • ▶sockaddr结构体
      • sockaddr结构体
      • sockaddr_in结构体
      • in_addr结构体
  • socket编程应用
    • ▶基于udp协议的socket编程
      • 服务端server_udp
      • 客户端client_udp
      • 地址转换函数
      • 结果展示
    • ▶基于tcp协议的socket编程
      • 服务端tcp_server
        • tcp是面向连接的--listen_sock介绍
      • 客户端tcp_client
        • tcp是面向连接的--connect介绍
      • 回调函数--处理sock带来的请求
    • ▶tcp与udp对比

在这里插入图片描述

预备知识

▶源IP地址与目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。这两个IP地址表述了这个数据包是从哪里来的,并且要到那里去,并且这两个地址表示的是数据包最初出发的地址,以及最终到达的地址;而实际上数据包在传输过程中并不止有这两个地址,还会有许多中间站,而用于表示数据包的上一站和下一站是源MAC地址和目的MAC地址。

▶端口号与进程id

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

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

▶源端口号与目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号。与源IP地址和目的IP地址类似,标识“数据是谁发的, 要发给谁”。

▶UDP与TCP协议

此处我们对udp和tcp协议有个直观的认识,后面详细讨论。

  • UDP协议(User Datagram Protocol 用户数据报协议)
    udp协议的特点是:面向数据报、无连接、不可靠的传输层协议。
  • TCP协议(Transmission Control Protocol 传输控制协议)
    tcp协议的特点是:面向字节流、有连接、可靠的传输层协议。

▶网络字节序

我们知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
先回顾一下何为大小端,对于一个整型,若高位在高地址处,低位在低地址处则为小端字节序;反之,若高位在低地址处,而低位在高地址处,则为大端字节序。在这里插入图片描述

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
    TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
    为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
    在这里插入图片描述
    这些函数名比较好记,比如h表示主机(host),n表示网络(network),l表示32位长整型(long int),s表示16位短整型(short int)。htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
    如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

套接字(socket)介绍

▶概念

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。
在这里插入图片描述

▶分类

socket主要有以下三种类型:

  • 1.数据报套接字(SOCK_DGRAM
    数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
  • 2.流套接字(SOCK_STREAM
    流套接字用于提供面向连接可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议。
  • 3.原始套接字(SOCK_RAM
    原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接字。

▶工作流程

要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket
对于面向连接的协议,套接字之间的连接过程可以分为三个步骤:

  • 1.服务器监听
  • 2.客户端请求
  • 3.确认连接
    在这里插入图片描述

socket编程

由于socket编程的模式比较套路化,基本可以根据模板写出,因此,先将socket套接字编程熟练,可以更好的理解后续的udp/tcp协议及网络传输原理。

▶socket编程接口

// 创建 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);

▶sockaddr结构体

socket的API是一层抽象的网络编程接口,适用于各种底层网络协议,比如IPv4、IPv6,以及后续的Unix Domain Socket。然而各种网络协议的地址格式并不相同,因此我们需要用一个sockaddr结构体来描述对应的网络协议,区分地址类型,并且描述其端口号与IP地址。在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INETAF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr*; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数。
  • 这里为什么不用void来强转的原因是最初网络提出socket套接字编程时还没有void类型,因此都是直接强制类型转换成sockaddr来使用接口的,而如果要替换为void,那么成本太大了。

sockaddr结构体

在这里插入图片描述
前者为16位的地址类型,后14字节位地址路径。

sockaddr_in结构体

在这里插入图片描述
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构体中出了16位的地址类型,还包含端口号和IP地址,以及8字节填充内容。

in_addr结构体

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

socket编程应用

▶基于udp协议的socket编程

由于udp协议是无连接的,因此udp的server与client不需要构建连接,直接客户端发出请求,服务器收到请求并处理。

服务端server_udp

//udp server.cc
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using std::cerr;
using std::cout;
using std::endl;

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

// ./server 8080
// proc port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    //1.创建套接字文件描述符
    int sock = socket(AF_INET, SOCK_DGRAM, 0);//udp: IPv4, 数据报

    //2.描述套接字端口号,IP地址信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local));//初始化为0
    local.sin_family = AF_INET;//16位地址类型
    local.sin_port = htons(atoi(argv[1]));//16位端口号,主机转网络
    local.sin_addr.s_addr = htons(INADDR_ANY);//IP地址使用INADDR_ANY,表示不绑定具体的IP地址,这样可以bind机器上的所有IP

    //3.bind socket与sockaddr_in结构体
    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        cerr << "bind error" << endl;
        return 2;
    }

    //4.启动服务,收发消息
    char buf[1024];
    while(true)
    {
        buf[0] = 0;
        sockaddr_in peer;//对端套接字
        socklen_t len = sizeof(peer);
        //从对端读取数据
        ssize_t s = recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer, &len);
        if(s > 0)
        {
            //success
            buf[s] = 0;
            cout << "client#" << buf << endl;//打印对端发来的数据
            std::string echo_message = buf;
            echo_message += " server received";
            //向对端发送数据
            sendto(sock, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&peer, len);
        }
    }
    close(sock);
    return 0;
}

这里需要注意,udp的服务端需要主动bind端口号与ip,但是这里bind的ip最好不要是具体的ip,因为一旦服务器bind的ip被占用,服务器就挂了,因此bind使用INADDR_ANY,可以bind云服务器的所有ip。
其次,这里介绍一下recvfrom接口:在这里插入图片描述

客户端client_udp

//udp client.cc
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using std::cout;
using std::cerr;
using std::endl;

void Usage(std::string proc)
{
    cout << "Usage\n\t" << proc << "dest_ip" << "dest_port" << endl;
}

// ./client 127.0.0.1 8080
// proc IP port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);//创建客户端套接字

    //client不需要我们自己去bind,实际上在sendto的时候,操作系统会自动随机给client bind端口号

    //绑定端口号和ip 确定具体某台主机上的某个进程,描述服务器端的ip和端口号
    struct sockaddr_in desc;
    bzero(&desc, sizeof(desc));
    desc.sin_family = AF_INET;
    desc.sin_port = htons(atoi(argv[2]));//绑定端口号
    //用户端需要bind具体的ip地址,这样才能够连接到对应的服务器端
    //ip本质上可以由4个字节保存 127.0.0.1 点分十进制,这是我们习惯的写法
    //但对于计算机而言,更希望看到的是32位的整型ip地址
    desc.sin_addr.s_addr = inet_addr(argv[1]);//inet_addr函数作用就是将点分十进制的ip地址转换为无符号的长整型

    char buf[1024];

    while(true)
    {
        buf[0] = 0;
        cout << "Please Enter#";
        fflush(stdout);
        ssize_t s = read(0, buf, sizeof(buf) - 1);
        if(s > 0)
        {
            buf[s - 1] = 0;// 将\n吸收
            //向对端发送数据,udp是无连接的,因此客户端直接向服务端发送请求
            sendto(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&desc/*向哪里发送*/, sizeof(desc)/*发送的长度*/);

            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            //从对端接收数据
            ssize_t size = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr*)&peer, &len);//peer与len暂且不用,仅作为接收使用
            buf[size] = 0;
            cout << buf << endl;
        }
    }
    close(sock);
    return 0;
}

客户端需要注意的点有:

  • 由于IP是点分十进制的传参,因此需要调用inet_ntoa转换为32为长整型。

地址转换函数

本文只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址
但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
在这里插入图片描述

  • 客户端无需主动bind端口号与ip,在向服务端发起请求调用sendto函数时OS会自动随机的为client去bind端口号与ip。这是因为不会有其他主机端口号来连接客户端,因此客户端无需自己有具体的ip与port,相反如果我们主动bind的话,还可能会bind失败。

结果展示

这里bind的ip为127.0.0.1,表示本地环回,即数据包绕本地一圈后到回来,用于测试udp协议的实现,可以看见,client向server发送的数据都被接收到并返回应答。
在这里插入图片描述

▶基于tcp协议的socket编程

这里我们实现一个简易字典的服务器功能。

服务端tcp_server

//tcp_server.hpp
#pragma once

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

using std::cout;
using std::cerr;
using std::endl;

namespace ns_TcpServer
{
    typedef void(*handler_t)(int);

    const int backlog = 5;
    class TcpServer
    {
    private:
        uint16_t port;
        int listen_sock;
    public:
        TcpServer(uint16_t _port)
        :port(_port)
        ,listen_sock(-1)
        {}
        void InitTcpServer()
        {
            listen_sock = socket(AF_INET, SOCK_STREAM, 0);//tcp: IPv4 流套接字
            if(listen_sock < 0)
            {
                cerr << "socket error" << endl;
                exit(2);
            }

            sockaddr_in local;
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);//端口号 主机转网络 短整型
            local.sin_addr.s_addr = htonl(INADDR_ANY);//ip地址,使用INADDR_ANY,可以bind 机器上任意一个ip

            //bind 套接字 端口号与ip
            if(bind(listen_sock, (sockaddr*)&local, sizeof(local)) < 0)
            {
                cerr << "bind error" << endl;
                exit(3);
            }

            //监听,tcp是面向连接的,服务器监听等待客户端来发起连接
            if(listen(listen_sock, backlog) < 0)
            {
                cerr << "listen error" << endl;
                exit(4);
            }
        }
        void Loop(handler_t handler)
        {
            //启动服务
            while(true)
            {
                sockaddr_in peer;
                socklen_t len = sizeof(peer);
                
                //建立好连接后,获取对端发来的消息
                int sock = accept(listen_sock, (sockaddr*)&peer, &len);
                if(sock < 0)
                {
                    cout << "warning:accept error" << endl;
                    continue;
                }

                handler(sock);//获取请求后回调函数处理请求

            }
        }      

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

tcp是面向连接的–listen_sock介绍

由于tcp协议是面向连接的,因此在双方进行通信之前,需要先建立连接,即server需要监听来自client的连接请求,因此在listen之前的套接字为监听套接字,而建立了连接之后用于获取对端消息的套接字才是和udp中作用一样的套接字。如何理解呢?
可以这么说,我们日常去饭店吃饭时,门口会站着揽客的服务员,而这些揽客的服务员拉到客人后,就交由饭店内的服务员来招待;而这里的listen_sock就类比作门口揽客的服务员,连接建立好后的sock就是招待的服务员。

//server.cc
#include "tcp_server.hpp"//提供网络连接功能
#include "handler.hpp"//提供处理网络套接字的功能

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

// ./tcp_server 8080
// proc port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
    }

    uint16_t port = atoi(argv[1]);

    ns_TcpServer::TcpServer* svr = new ns_TcpServer::TcpServer(port);

    svr->InitTcpServer();

    svr->Loop(ns_handler::Handler_V1);//单执行流

    return 0;
}

客户端tcp_client

#pragma once

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


using std::cout;
using std::cerr;
using std::endl;

namespace ns_TcpClient
{
    class TcpClient
    {
    private:
        std::string dest_ip;
        uint16_t dest_port;
        int sock;
    public:
        TcpClient(std::string _ip, uint16_t _port)
        :dest_ip(_ip)
        ,dest_port(_port)
        ,sock(-1)
        {}
        void InitClient()
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);// IPv4 流套接字
            if(sock < 0)
            {
                cerr << "socket error" << endl;
                exit(2);  
            }

            //client 无需主动bind, connect时OS会自动进行相关的bind
            //但是client需要connect,发起与服务器的连接
        }
        void Start()
        {
            //发起连接,填充对端服务器的socket信息
            sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            peer.sin_family = AF_INET;
            peer.sin_port = htons(dest_port);//端口号
            peer.sin_addr.s_addr = inet_addr(dest_ip.c_str());//ip

            //发起连接请求
            if(connect(sock, (sockaddr*)&peer, sizeof(peer)) == 0)
            {
                cout << "connect successfully..." << endl;
            }
            else
            {
                cout << "connect failed" << endl;
                exit(3);
            }

            //连接成功,实现业务逻辑
            while(true)
            {
                char buf[1024];
                cout << "Please Enter#";
                fflush(stdout);
                ssize_t s = read(0, buf, sizeof(buf) - 1);//从标准输入中读取数据
                if(s > 0)
                {
                    //read success
                    buf[s - 1] = 0;//吸收'\n'
                    //向对端发送消息
                    send(sock, buf, strlen(buf), 0);
                    ssize_t size = recv(sock, buf, sizeof(buf), 0);//从对端接收消息
                    if(size > 0)
                    {
                        //recv success
                        buf[size] = 0;
                        cout << buf << endl;
                    }
                    else
                    {
                        cout << "server close..." << endl;
                        break;
                    }
                }
            }
        }
        ~TcpClient() {if(sock >= 0) close(sock);}
    };
}

tcp是面向连接的–connect介绍

同样的,对于client,在与服务器进行通信之前,需要发起连接请求,即:
通过传入描述服务器的套接字信息,OS会自动为client bind相关的信息,同时向套接字描述的对象发起连接请求。

#include           
#include 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//client.cc
#include "tcp_client.hpp"


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

// ./client 127.0.0.1 8080
// proc desc_ip desc_port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
    }

    std::string dest_ip = argv[1];

    uint16_t dest_port = atoi(argv[2]);

    ns_TcpClient::TcpClient cli = ns_TcpClient::TcpClient(dest_ip, dest_port);

    cli.InitClient();

    cli.Start();

    return 0;
}

回调函数–处理sock带来的请求

这里我们实现3个版本的回调函数方案,但第一种方案不能处理多连接请求。

#pragma once

#include "tcp_server.hpp"
#include 
#include 
#include 
#include 

std::unordered_map<std::string, std::string> dict = {
    {"sort", "排序"},
    {"left", "左边"},
    {"map", "地图,映射"},
    {"free", "自由,免费"},
    {"comfortable", "舒服的"}
};

namespace ns_handler
{
    void Handler(int sock)
    {
        //回调函数被调用
        while(true)
        {
            //函数处理客户端发来的请求
            char buf[1024];
            //从sock套接字文件描述符获取请求
            ssize_t s = recv(sock, buf, sizeof(buf), 0);//这里后面会介绍,其实是应用层从传输层的缓冲区将客户端发来的消息拷贝到应用层
            if(s > 0)
            {
                //recv success
                buf[s] = 0;
                cout << "client#" << buf << endl;
                std::string word = buf;
                auto ret = dict.find(word);
                std::string echo_message = "I don't know.";
                if(ret != dict.end())
                {
                    echo_message = ret->second;
                }
                send(sock, echo_message.c_str(), echo_message.size(), 0);
                cout << "server#" << echo_message << endl;
            }
            /*if(s > 0)
            {
                //recv success
                buf[s] = 0;
                cout << "client#" << buf << endl;
                std::string echo_message = buf;
                if(echo_message == "quit")
                {
                    cout << "client quit..." << endl;
                    break;
                }
                echo_message += " server_received\n";
                //向对端发送数据(回写)
                send(sock, echo_message.c_str(), echo_message.size(), 0);
            }
            else if(s == 0)
            {
                //对端链接关闭
                break;
            }
            else
            {
                cerr << "recv errno" << " errno: " << errno << endl;
                //读取失败
                break;
            }*/
        }
    }

    void Handler_V1(int sock)
    {
        //version1:单执行流
        Handler(sock);
    }

    void Handler_V2(int sock)
    {
        //version2:多进程版本
        if(fork() == 0)
        {
            //child
            if(fork() == 0)
            {
                //grandchild
                Handler(sock);//子进程退出,孙子进程成为孤儿进程被OS领养,因此无需wait该进程
            }
            else
            {
                exit(0);
            }
        }
        //father 为了防止父进程阻塞式的等待子进程结束,这里让子进程创建孙子进程
        //然后孙子进程回调函数,子进程退出,孙子进程成为孤儿进程,从而无需关心其退出状态
        waitpid(-1, nullptr, 0);
    }

    void* rountinue(void* args)
    {
        int sock = *(int*)args;
        delete (int*)args;
        pthread_detach(pthread_self());
        Handler(sock);
        close(sock);//处理业务完后,关闭文件描述符,防止文件描述符泄露
        return nullptr;
    }

    void Handler_V3(int sock)
    {
        pthread_t pid;
        int* p = new int(sock);//暂时这么处理
        pthread_create(&pid, nullptr, rountinue, p/*this*/);
    }
}

对于第一种单执行流的版本,我们通过多个客户端去连接发送消息:
在这里插入图片描述
对于第二种和第三种方案,则是通过多进程和多线程的方法来保证服务器可以同时处理多个请求的情况。
其次,在多进程版本下,如果仅仅是让服务器作为父进程创建子进程,那么父进程阻塞式等待子进程退出时也不能处理其他请求,因此这里采取一种取巧的方法,即让子进程创建孙子进程去执行请求,子进程立即退出,就可以避免父进程阻塞,同时孙子进程成为孤儿进程,无需担心其成为僵尸进程。在这里插入图片描述
但实际上,多进程的资源消耗比较大,因此采用多线程是更加高效的,不过需要注意的是,多线程中可能存在传参的问题,即栈上的参数传入函数后,导致变量丢失,这里暂时处理为在堆上开辟空间。另外,线程执行回调函数结束后需要关闭文件描述符,防止文件描述符泄露(前面没有关闭文件描述符是因为进程结束后文件描述符自动释放了,而线程结束文件描述符不会释放,导致这些文件描述符无法再被利用,从而泄露)。
在这里插入图片描述
但其实多线程版本也存在缺陷,就是服务器永不结束,那么这样创建的线程是无上限的,一旦请求太多,可能出现严重的问题,因此这里最佳方案是线程池版本,如果有兴趣可以自己实现一下。

▶tcp与udp对比

其实通过上面的内容就大致了解tcp与udp的区别了:

  • tcp是面向连接的,而udp是无连接的。
  • tcp是字节流套接字,而udp是数据报套接字。
  • tcp是可靠的,而udp是不可靠的。
    这里需要提一下的是,tcp的可靠和udp的不可靠属于中性词,并非说tcp就比udp要好;其次tcp的可靠性保证是有代价的,那么udp就会在性能上优于tcp,比如直播时不一定要保证画面的精确传输,那么使用udp可以是延迟没那么大。当然tcp的性能也并不是完全劣于udp的,这里我们后面介绍传输层时会具体谈的。
    总的来说,网络套接字编程内容大概就是这样了,由于套接字编程相对套路化,因此多写才能更加熟练并深入理解。

你可能感兴趣的:(Linux,笔记,网络,linux,udp,tcp/ip,网络协议)