网络套接字编程

本文介绍了网络通信的基础知识,包括IP地址、端口号的概念,以及TCP和UDP协议的特点。详细讲解了TCP的三次握手和UDP的四次挥手过程。还展示了简单的UDP服务器和客户端的C++实现,并提及了TCP服务器的创建。最后讨论了守护进程的创建及其在服务器中的重要性。

       之前我们粗浅的认识了一下网络的一些知识,比如OSI七层模型,TCP/IP四层模型,那么我们具体怎么实现两台主机的交互呢?

        在学习这些之前,我们需要准备一些预备知识。

目录

预备知识

1:认识源IP地址和目的IP地址

2:认识端口号(port) 

3:理解端口号和进程ID

4:认识源port和目的port

认识TCP协议

认识UDP协议

网络字节序

SOCKET编程接口

 

简单的UDP程序

简单的UDP服务器

IP地址简单讲解

简单的UDP客户端

简单的TCP程序

TCP服务器

TCP客户端

守护进程

        会话,前台任务和后台任务

 

如何创建守护进程

        忽略异常信号

        不可选择组长

        关闭或者重定向默认打开的文件

TCP通讯流程

三次握手

服务器初始化

建立连接

 

四次挥手

断开连接

TCP协议和UDP协议对比


 

预备知识

1:认识源IP地址和目的IP地址

        在我们的IP数据包报头中,有两个IP地址,一个是源IP地址,一个是目的IP地址,而这些IP我们一般称之为公网IP,用来标示一台唯一的主机的。        

源IP地址:标示数据包的发送主机

目的IP地址:标示数据包的接收主机

 

        但是我们在日常生活中使用手机或者计算机,实际上并非只需要有对方的IP地址就能够相互交流,在日常生活中,我们都是通过微信,或者QQ之类的软件才能相互之间发送数据。

        而只有一个IP地址显然是不够用的,那么就轮到端口号出场了。

2:认识端口号(port) 

端口号 :端口号是传输层协议的内容,它是一个两字节16位的整数(uint16_t),用来标示一个进程,用来告诉操作系统这个数据要交给哪个进程来处理,并且一个端口号只能由一个进程占用。

         当我们使用 IP+port 就能标示网络上的某一台主机上的某一个进程,这样用户就能够成功建立通信。

        说到这里我们就应该明白,实际上网络通信的本质就是进程间通信。

        port 用来标示进程的唯一性,而 pid 也是用来标示进程的唯一性的,这两者有什么关联吗?

3:理解端口号和进程ID

        我们都知道在主机中,pid 是用来标示进程的唯一性的,这里的 port 号也是用来标示进程的唯一性的,那么为什么不直接用 pid 来表示 port 呢?

理由

1系统是系统,网络是网络,需要将系统和网络解耦

2:客户端每次都需要找到服务器进程,就决定了服务器的唯一性不能改变,而 pid 是能够轻易改变的

4:认识源port和目的port

        源port 就是指发送数据的进程,而目的 port 就是接收数据的进程。

 

        当我们了解了IP地址和 port 之后,我们就需要初步了解下网络的两个协议——TCP和UDP

 

认识TCP协议

        TCP协议,又叫传输控制协议,它是传输层的协议,具有以下特点

1.有连接——在IO之前需要先建立连接

2.可靠传输——当出现错误的时候能够有相应的策略应对

3.面向字节流——通过字节流进行IO

认识UDP协议

        UDP协议,又叫用户数据报协议,它也是传输层的协议,具有以下特点

1.无连接——IO之前不需要建立连接

2.不可靠传输——没有相应的错误应对策略

3.面向数据报——通过数据报进行IO

 

网络字节序

        我们都知道,在内存中,对于多字节数据的存储分为大端和小端,而网络的数据也有大小端之分。

        TCP/IP协议规定,任何一台主机,它的网络数据流都采用大端字节序。

        而为了使网络程序具有可移植性,有以下库函数做网络字节序和主机字节序的转换。

#include<arpa/inet.h>

uint32_t htonl(uint32_t hostlong);//主机字节序转网络字节序——长整型

uint16_t htons(uint16_t hostshort);//主机字节序转网络字节序——短整型

uint32_t ntohl(uint32_t netlong);//网络字节序转主机字节序——长整型

uint16_t ntohs(uint16_t netshort);//网络字节序转主机字节序——短整型

        虽然看上去比较难记,但实际上很好记,其中 h 表示 host ,也就是指主机,n 标示 net,也就是指网络,s 指 short,就是指16位的整数,l 是指 long ,就是指32位的整数。

 

SOCKET编程接口

        socket 这个东西想必大家都很陌生。

        在上面我们都知道,两个主机上的进程想要通信,就一定需要知道对方主机的 ip 和进程的 port,而 socket 实际上就是指 ip + port ;

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

        这些接口有点只能在TCP协议中用,有的TCP和UDP都能使用,有的只能在服务器上用,有点能在客户端上用,根据场景不同使用。 

        而在这些函数中,我们都看见一个参数类型是 struct sockaddr* 类型的,这个类型我们也需要了解。

        socket API 是一层抽象的网络编程接口,适用于各种底层网络协议。

        但是各种网络协议的地址各不相同。

ffc96f227d324631aa028f8ed0de8ada.png

         在上图中,我们发现最左边的sockaddr是函数中所需要传参的变量类型,而右边则是两种没有看到过的类型。

        实际上这是这套接口的发明者所用的一个小技巧,他将所有接收的变量类型定义为sockaddr,而不同协议则需要使用不同的 sockaddr,例如TCP和UDP协议都是使用的sockaddr_in的类型,而sockaddr_un 则有不同的协议使用。

        因此,我们在外部调用这类函数的时候,只需要根据自己使用的协议来使用不同的sockaddr类型,然后在里面填充数据后强转再传参,而具体的操作和判断就交给函数内部进行。

 

sockaddr结构

9c65e9def286405b83a91af1c86584bf.png

 

sockaddr_in结构

f72c3f2cb7cc4fa18fcb441316dd1ebc.png

 

        当我们有了以上的预备知识后,我们便先来学习下UDP协议下的通信以及代码实现。

 

简单的UDP程序

        经过上面的预备知识,我们知道,要进行网络通信,那么一定是需要 IP地址 和 端口号 port 的,而且socket 套接字也是必不可少的,因此,我们想要编写一个UDP程序,这三者缺一不可。

简单的UDP服务器

我们首先以UDP服务器的编写为示范。 

3ff84ea0c6734f1aa2614a881263ba24.png

 

        在定义好UDPSever的成员变量后,便是需要对这些变量初始化。

 20d56e7bc11d41aea48748c168755ecb.png

         这里我们依靠外面传参决定服务器使用几号端口进行通信,而 IP 地址,我们默认设为 0.0.0.0;

        至于为什么 IP 是 0.0.0.0,则在后面进行解答。

 

        在初始化完成员变量后,我们便是需要将服务器给初始化完成。

48df69560157434bb9a72bbb5e5af635.png

 

        在start函数中,我们首先创建了一个 socket 套接字。

        其中,我们传了三个参数:AF_INET,SOCK_DGREAM,0。

        先来了解这三个参数分别代表什么吧。

b623efbc631e47f99e4ee697c637a50b.png

          首先第一个 domain 参数,表示我们是想进行网络通信还是本地通信。这里我们的domain 参数是 AF_INET,表示我们是网络通信,且IP地址是 IPv4 地址格式。

        而第二个参数 type 表示我们的socket 所提供的能力类型,即是使用流式套接字还是使用用户数据报套接字。

        而第三个参数protocol 则是表示我们采用TCP还是UDP,不过实际上我们前两个参数就已经决定我们使用什么协议了,因此我们这里默认给 0;

 

        而在拿到套接字之后,我们需要将 ip 地址和 port 号绑定给该套接字中,而要将 ip 地址 和 port 号两个变量绑定给套接字,我们需要使用bind函数。

f6cba383e9b74c8e9c1642e547e2e0a9.png

         bind 函数需要三个参数:sockfd, addr, addrlen。

        这里需要提一嘴的是,实际上我们在上面所申请的 socket 实际上类似于文件描述符,如果说之前的socket函数的操作都属于文件范畴,那么这里的 bind 函数的操作就属于网络操作的范畴。

        它会告诉操作系统,这个套接字是属于哪个端口号的,这样我们才能成功的通过套接字来进行网络通信

        而addr 这个变量在预备知识我们已经了解过,我们并非是使用 sockaddr 这个类型,而是使用sockaddr_in 这个类型。

        预备知识中也说过,sockaddr_in这个类型需要我们手动填充,我们首先将协议定为AF_INET

        然后将 IP 地址通过 inet_addr 转换后,填入其中。

        关于IP地址这里需要简单讲解一下。

IP地址简单讲解

        IP地址分为点分十进制风格和整数风格。

       其中点分十进制是一个字符串,可读性好,而整数风格的IP地址则用于网络通信。

        因此我们的IP地址都是需要通过函数转换一下才能使用。

      

        此外,我们在前面的预备知识都了解过,网络也有自己的字节序,因此我们的port 端口号需要使用的话,也是需要通过相应的函数进行转化才能在网络上使用。

        当我们填充完该变量后,就只需要直接将该变量和socket直接绑定即可。       

        至此,我们的服务器就初始化完成了。

   

     接着我们就需要让服务器开始运行。

      5d74acce8f3346749a9daeadb70dcfff.png

 

 

      在 start 函数中,我们使用buffer作为缓冲区,用recvfrom函数从套接字中接收数据,把数据放入buffer 中,并且用peer变量来接收客户端的 ip 地址 和 port 号。

        虽然服务器并不主动开始通信,但是服务器有时需要返回数据,到时候就需要使用客户端的 ip 地址 和 port 号。

        接着我们用 cip 和 cport 分别将peer 中的字段转换后接收,并且输出客户端发送的信息。

        一个简易版的UDP服务器就完成了。

 

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <string.h>

using namespace std;



namespace UDP
{
   static const string defaultip = "0.0.0.0";
    class Sever
    {
    
    public:
        Sever(uint16_t port, string ip = defaultip)
            : _port(port), _ip(ip)
        {
        }

        void start()
        {
            //初始化套接字
            _socket = socket(AF_INET,SOCK_DGRAM,0);
            if(_socket == -1)
            {
                cerr<<"socket err!"<<endl;
                exit(-1);
            }
            cout<<"socket successed!"<<endl;
            //绑定IP 和 port
            struct sockaddr_in local;
            bzero(&local,sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = inet_addr(_ip.c_str());
            local.sin_port = htons(_port);
            int n = bind(_socket,(struct sockaddr*)&local,sizeof(local));               
            if(n == -1)
            {
                cerr<<"bind failed!"<<endl;
                exit(-1);
            }
            cout<<"bind successed!"<<endl;


        }

        void run()
        {
            while(true)
            {
                char buffer[1024];
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                bzero(&peer,sizeof(peer));

                size_t n = recvfrom(_socket,buffer,sizeof(buffer),0,(sockaddr*)&peer,&len);
                if(n == -1)
                {
                    cout<<"recvfrom failed!"<<endl;
                    exit(-1);
                }
                cout<<"recvfrom successed!"<<endl;
                string cip = inet_ntoa(peer.sin_addr);
                uint16_t cport = ntohs(peer.sin_port);

                buffer[strlen(buffer) - 1] = 0;

                cout<<"Client["<<cip<<"]["<<cport<<"] # "<<buffer<<endl;
            }
        }
        ~Sever()
        {
        }

    private:
        int _socket;
        string _ip;
        uint16_t _port;
    };
}

          

        接着我们就需要写一个外部调用代码。

        我们在UDPSever.cc中写下如下代码,就能够成功运行了。

#include"UDPSever.hpp"
#include<memory>


using namespace UDP;

int main(int args,char* argv[])
{
    if(args != 2)
    {
        cout<<"./UDPSever port"<<endl;
        exit(-1);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<UDP::Sever> usp(new Sever(port));
    usp->start();
    usp->run();
    return 0;
}

 

        写完服务器,接着就是客户端了。

简单的UDP客户端

97421fe0fa6b4fd3ba12951fc2f343bc.png

        一个客户端和服务器都是需要相同的成员变量,但是不同的是客户端的IP地址不能为0.0.0.0;

而必须是服务器的IP地址,这样才能使得客户端成功连接到服务器。

        而之后的操作,客户端和服务器基本一致。

        客户端的初始化相比服务器而言,少了绑定操作,因为客户端一般是主动向其他主机发送连接请求,只需要通过函数发送数据即可。 

 decdd2bac4f34f06aa143a1cec02d052.png

 

        然后就是客户端的运行了。

80b13b02a1d54987aee6ea9f9fe7850d.png

         我们也是和服务器一样的操作,不过不同的是,客户端是将数据送出去,而非拿进来。

       

#include <iostream>
#include <string>
#include <stdlib.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>

using namespace std;

namespace UDP
{
    class Client
    {
    public:
        Client(string ip, uint16_t port)
            : _ip(ip), _port(port)
        {
        }
        void start()
        {
            _socket = socket(AF_INET, SOCK_DGRAM, 0);
            if (_socket == -1)
            {
                cout << "socket failed!" << endl;
                exit(-1);
            }
            cout << "socket successed!" << endl;
        }

        void run()
        {
            struct sockaddr_in peer;
            peer.sin_family = AF_INET;
            peer.sin_addr.s_addr = inet_addr(_ip.c_str());
            peer.sin_port = htons(_port);


            while (true)
            {
                    char buffer[1024];
                    cout<<"Enter # "<<endl;
                    fgets(buffer,sizeof(buffer),stdin);
                    sendto(_socket,buffer,sizeof(buffer),0,(sockaddr*)&peer,sizeof(peer));
            }
        }
        ~Client()
        {
        }

    private:
        int _socket;
        string _ip;
        uint16_t _port;
    };
}

        此外就是客户端的外部调用了。

#include"UDPClient.hpp"
#include<memory>

using namespace UDP;

int main(int args,char* argv[])
{
    if(args != 3)
    {
        cout<<"./UDPClient ip port"<<endl;
        exit(-1);
    }
    string ip = argv[1];
    uint16_t port =  atoi(argv[2]);
    unique_ptr<Client> upc(new Client(ip,port));
    upc->start();
    upc->run();

}

        这样我们的简单的UDP客户端就完成了。

        我们来看看成果吧!

9ab517522fe24592b2075ee76c0b941d.png

         由于笔者是在一台电脑上进行通信,因此这里客户端输入的 IP 地址为 127.0.0.1,即本地回流,表明接收的IP地址是本机。

        此外,客户端上输入的端口号应该和服务器的端口号相同,因为客户端输入的IP地址和端口号都是服务器的IP地址和端口号。

        那么这里就有一个问题,既然我们客户端所有初始化的 IP 地址 和 端口号都是服务器的,那么服务器又是怎么知道客户端的 IP 地址和 端口号的呢?

        实际上,客户端在发送数据之前,操作系统会自动选择一个可用的本地IP地址和端口号来和目标主机建立连接,因此不用我们手动建立连接。

       

简单的TCP程序

        

TCP服务器

        在做完一个简单的UDP协议服务器和客户端后,自然就要试试写一个TCP服务器了。

        

        实际上TC服务器和UDP服务器的代码十分相似,只有个别不同,我们直接看代码。

8e0de0df7d9441a6bf5af9dd2325a4bf.png       在代码中我们能够看到,TCP服务器的初始化和UDP服务器的初始化是十分相似的,只有两点不同。

 

1:socket函数创建套接字的时候,第二个参数是SOCK_STREM。

2:初始化时有listen函数,来设置套接字的状态。

        

        关于为何socket函数第二个参数是SOCK_STREM,这是因为TCP协议是面向字节流的,因而使用SOCK_STREM。

        而listen函数也是因为TCP协议是面向连接的,listen函数使套接字处于监听状态。

a4fd5d2c2c794cb9847592440fc5529b.png

        listen函数一共两个参数,第一个就是需要改变状态的socket,第二个参数表明最多允许backlog个客户端处于连接等待状态,连接到更多请求就会忽略。

 

        将服务器初始化后,我们就需要写运行代码。

        263a7b6fb35c4684ab53deeeb200c314.png

 

        在运行代码中,我们发现,有一个accept函数,我们先来看一下这个函数是干嘛的。、

496bf7f3593b4f14ba26f41998ebd36c.png         对于accept函数,它的三个参数我们都不陌生,第一个就是进入监听状态的listensocket,后面两个参数我们在UDP协议中使用recvfrom的时候就已经见识到了。

        重点在于accept函数的返回值。

e1733b362113422aa89862b6afc65cb9.png         根据手册的描述,accept函数返回值也是一个socket套接字!

        那么这个返回的socket和我们设为监听的listensocket又有什么区别呢?

        实际上,listensocket的作用并不是用来通信的,是用来监听是否有新的连接到来。

        而真正进行通信的实际上是accept函数返回的socket

 

15609d5e56474de493375310f8929874.png

        紧接着,我们就能够使用seviceIO函数,在函数内部进行真正的通信。

        我们的函数内部使用read函数从socket里面接收信息,从而成功通信。

        然而,我们这里有一串代码需要详细的讲一下:

ab7a9607e7594e74bf6ac20cd55ce710.png

 

         这串代码从字面意思上来看,就是父进程创建一个子进程,然后在子进程里面创建一个孙子进程,然后将子进程关闭,由孙子进程进行通信,然后父进程回收子进程,这是为什么呢?

        首先我们都知道,子进程在退出的时候,若是父进程不等待,就会导致子进程变成僵尸进程,从而导致资源泄露等问题。

        但是,如果我们的父进程等待子进程,就会造成串行的问题,因为等待是阻塞的,这样我们的服务器就不能对多个客户端请求进行处理。

        因此,我们这里创建了一个孙子进程,然后将子进程退出,这样孙子进程就会被1号进程领养,当孙子进程退出时,会自动回收资源,而父进程在子进程退出时就已经回收它的资源了。

        这样就不会出现串行的问题,也不会出现资源泄露的问题。

        当然,这里有多种写法,我们可以使用多线程的写法,就不用关心这么多了。

 

TCP客户端

        对于客户端,实际上也和UDP协议相差不多。

f7076d859c6245c29044753c16fc4e99.png

 

        首先是客户端初始化的时候,创建套接字。

        其次便是运行。3791b2b48d834af4b466fd77cd07f6dc.png 

         我们发现TCP的客户端比UDP的客户端多一个connect的步骤,实际上也是因为TCP是面向连接的,因此需要用connect与服务器建立连接。

        我们先来看看connect的描述。

52612d15e034415882061515a9babef7.png

        实际上connect的具体用法相信不用说都能够明白了,重点是这一串描述。

ec5509dcb49a44d6b19ac9a68fac826e.png         这句话说:如果连接或者绑定成功了,返回0,错误返回1.

        也就是说,TCP客户端的隐式绑定是在connet的时候发生的,这和UDP客户端不同,UDP绑定是在客户端初次sendto的时候绑定的。

 

        不过需要注意的是,客户端所保存的ip和port都是服务器的。

 

        接着我们就能顺利运行起来了。

46fd1509767f4a1897528a49cefe194e.png

         

       

守护进程

        目前,我们已经将TCP和UDP的服务器和客户端分别写了一遍,但是,真正的服务器并不是像我们这样,将程序一关就结束了,这个时候就需要讲一讲守护进程了。

        在了解守护进程前需要一些预备知识。

        会话,前台任务和后台任务

                248058e9e67f45809e6533cf4ee30eaa.png

        当我们打开xshell连接到服务器时,服务器内部会创建一个会话,然后再在会话里面创建一个bash,也就是命令行解释器,其中,bash就是一个前台任务。

                在会话中,只能有一个前台任务,但是可以有多个后台任务。 

        而我们的后台任务可以创建多个。

506847fb87d5447fac4e47dca4badbd8.png

        我们创建两串进程组,分别是sleep 10000| sleep 20000| sleep 30000 和 sleep 40000| sleep 50000| sleep 60000,然后通过 & 将它们放到后台去。并且通过jobs指令查看到他们都在后台中。

        而这两串进程组就是后台任务。

        然后我们通过 ps 命令查看这些后台任务的时候,发现一件事。

907d345f100a4d45b334856add3cf233.png         这些后台任务有一个PGID,并且这两个进程组的PGID各不相同。

        实际上PGID指的是该进程组的组长的PID,我们可以看到都有对应的进程ID。

        此外,我们发现PGID旁边还有一个SID,而且这两个进程组的SID都一样,SID又是什么呢?

                                        SIG:指该会话(session)的ID

         此时,若是我们将后台进程变为前台任务就会发现我们的命令行解释器bash失效了。

5cd778ebd4724480b82e5138cc60bf75.png

         通过fg + 后台任务号,我们将2号后台任务提到前台中,发现bash命令都不能用了,然后ctrl+z将2号任务暂停,才能够使用bash,然后使用bg + 后台任务号让2号任务继续运行起来。

        

        而我们发现,前台任务和后台任务都是受用户的退出和注销的影响的,而想避免这种影响,就必须自成会话,自成进程组,和终端设备无关,这就是守护进程。

 

如何创建守护进程

 

        创建守护进程需要我们自己写一个函数。

        创建守护进程一共三步:

       1.忽略异常信号

       2.不可选择组长

       3.关闭或者重定向进程默认打开的文件

        忽略异常信号

       有时候当一个客户端在写,而服务器未接收时,服务器会收到信号从而导致服务器崩溃,因此需要忽略异常信号。

        不可选择组长

         创建守护进程最重要的就是setsid函数,他会将调用该函数的进程设置为守护进程,但是前提是该进程不能是组长。

f343d9521b034837a43bf245ca7c1a8f.png

         该函数的描述中就确定不能是组长,否则会报错。

        关闭或者重定向默认打开的文件

        在这里我先隆重讲解一下传说中的文件黑洞——/dev/null

        想要创建一个守护进程,就必须将该进程的三个文件描述符全部重定向到该文件。

                /dev/null是一个文件黑洞,任何放入其中的文件都会被丢弃。

       说完三个需要注意的点,我们直接看代码。

#pragma once

#include <unistd.h>
#include <signal.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;
#define DEV "/dev/null"

void daemonSelf()
{
    // 1.忽略异常信号
    signal(SIGPIPE, SIG_IGN);
    // 2.让自己不成为组长
    if (fork() != 0)
        exit(0);
    pid_t n = setsid();
    assert(n != -1);
    // 3.关闭或者重定向进程默认打开的文件
    int fd = open(DEV,O_RDWR);
    if(fd >= 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);

        close(fd);
    }
    else 
    {
        close(0);
        close(1);
        close(2);
    }
}

         首先第一步,我们直接忽略掉异常信号SIGPIPE;

        其次,我们fork一下,让子进程调用setsid,这样就不是组长进程了;

        最后,我们打开文件黑洞,然后将文件都重定向到里面,否则就关闭它们。

        这样就完成了,然后在服务器的调用中使用该函数即可。

d6de97c5296d4c8bb62fdb5b8bd23092.png

        然后我们看看表现。

 55f49d326ae341ffba53d30c90a017e8.png

         我们发现确实这个进程不是前台任务了,而且它的PID,PGID和SID都是它自己,说明它确实已经成为了守护进程了!

 

TCP通讯流程

        

        TCP的客户端和服务器的通信流程用一句话就是三次握手和四次挥手。

2d0e3567df3942418bbb4a31282f8d15.png

 

三次握手

       

服务器初始化

1.通过socket函数创建文件描述符。

2.调用bind函数,将该文件描述符和ip与port绑定,若port号被占用,绑定失败。

3.通过listen函数,声明该文件描述符作为服务器的文件描述符,为accept做准备。

4.阻塞等待accept返回,等待客户端连接。

 

建立连接

1.当客户端初始化后,通过connect向服务器发起连接请求。

2.connect发送SYN段并且阻塞等待服务器应答(第一次)。

3.服务器收到SYN段会返回SYN-ACK表示同意建立连接(第二次)。

4.客户端收到SYN-ACK段从connect函数返回,并且应答一个ACK段(第三次)。

 

        当客户端和服务器通过三次握手建立连接后,两边可以重复通过write和read函数进行通信。直到调用close函数关闭文件描述符。

 

四次挥手

断开连接

1.当客户端调用close关闭连接时,客户端向服务器发送FIN段(第一次)。

2.服务器收到FIN段后,会回应ACK段,同时read返回0(第二次)。

3.read返回后,服务器就知道客户端断开连接,会调用close断开连接,同时发送FIN(第三次)。

4.客户端收到FIN,再返回一个ACK给服务器(第四次)。

        通过四次挥手,客户端和服务器就断开了连接。

 

TCP协议和UDP协议对比

TCP特点

1.字节流。

2.面向连接。

3.可靠传输。          

 

UDP特点

1.用户数据报。

2.无连接。

3.不可靠传输。 

        也许这里大家看到UDP是不可靠的时候,就会认为UDP不好用,其实这里的不可靠是个中性词,不可靠意味着UDP简单,快速,而TCP可靠也意味着TCP复杂。

        因此大部分时间都是通过具体环境使用两种协议。 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值