Linux网络编程 - 基于TCP的服务器端/客户端(1)

2023-11-04

一 理解 TCP 和 UDP

        根据数据传输方式的不同,基于网络传输协议的套接字一般分为TCP套接字和UDP套接字。因为TCP是面向连接的,因此又称为基于流(stream)的套接字。

  • TCP(Transmission Control Protocol,传输控制协议) 意为“对数据传输过程进行控制”。
  • UDP(User Datagram Protocol,用户数据包协议)

1.1 计算机网络体系结构

        计算机网络是一个非常复杂的系统。目前全世界最大的计算机网络系统叫因特网(Internet),现在人们普遍称之为互联网,它是一个覆盖了全球绝大地区的互连网络,是由数量极大的各种计算机网络互连起来的。互联网具有两个主要基本特点,即连通性共享

        为了设计出极其复杂的计算机网络,工程师们提出了分层的方法。“分层”的好处是可以将一个庞大而又复杂的问题,转化为若干较小的局部问题,而这些较小的局部问题就比较容易研究和处理了。

        目前的计算机网络体系结构的划分有三种模型,分别是:TCP/IP 4层模型、TCP/IP 5层模型、OSI 7层模型。如下图所示:

图1-1  计算机网络体系结构

 《说明》实际互联网上使用的是 TCP/IP 4层协议体系结构,而 TCP/IP 5层协议体系结构一般只是教材为了介绍计算机网络原理而设计的。OSI 的7层协议体系结构,是国际标准化组织(ISO) 提出的一个计算机网络体系结构的全球性标准框架,即著名的开放系统互连基本参考模型 OSI/RM(Open Systems Interconnection Reference Model),简称为 OSI。OSI/RM 在1983年形成了开放系统互连基本参考模型的正式文件,即著名的 ISO 7498 国际标准,也就是所谓的7层协议的体系结构

        OSI 7层体系结构模型试图达到一种理想境界,即全球计算机网络都遵循这个统一的标准,因而全球的计算机将能够很方便地进行互连和交换数据。然而事与愿违,由于基于 TCP/IP 4层协议的网络体系结构的互联网已抢先在全球相当大的范围内成功地运行了,占领了市场,所以几乎没有什么网络设备厂商生产出符合 OSI 标准的商用产品。最终,OSI 只获得了一些理论研究成果,但在市场化方面则失败了。现今规模最大的、覆盖全球的、基于 TCP/IP 的互联网并未使用 ISO 标准。所以,我们学习计算机网络编程,也是基于 TCP/IP 协议栈体系结构为基础的。

1.2 TCP/IP协议族体系结构

        为了便于学习和理解计算机网络的分层体系结构,我们以五层协议的体系结构为模型,该体系模型综合了 OSI 和 TCP/IP 的优点,是一种很好的折中办法。下面我们自上而下、简要地介绍一下各层的主要功能。

  • 应用层(application layer)

        应用层是体系中的最高层,是为用户提供所需要的各种应用服务,例如:FTP、Telnet、DNS、SMTP等,它是用户与网络的接口。该层通过应用程序来完成用户的网络应用需求,如:文件传输、收发电子邮件等。

        功能:通过应用进程间的交互来完成特定网络应用服务。

        应用层协议(application layer protocol):不同的网络应用的应用进程之间,需要有不同的通信规则,这就需要有应用层协议。应用层协议定义的是应用进程间通信和交互的规则。这里的进程就是指主机中正在运行的应用程序。每一个应用层协议都是为了解决某一类应用问题,而问题的解决又必须通过位于不同主机中的多个应用进程之间的通信和协作来完成。应用进程之间的这种通信必须严格遵循规则。应用层的具体内容就是精确定义这些通信规则,即应用层协议。在互联网中的应用层协议有很多,如域名系统 DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议,等等。

        数据传输单元:报文(message)

        题外话:网络编程的大部分内容就是设计并实现应用层协议

  • 运输层(transport layer)

        运输层也叫传输层,运输层的任务就是负责向两台主机中应用进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。所谓“通用的”,是指并不针对某个特定的网络应用,而是多种应用可以使用同一个传输层服务。也就是说,运输层是为应用层服务的。

        功能:为应用进程之间提供端到端的逻辑通信服务。之所以说是为上层的应用进程之间提供端到端的逻辑通信服务,是因为从运输层的角度看,通信的真正端点并不是主机而是主机中的进程。也就是说,端到端的通信是应用进程之间的通信。“逻辑通信”的意思是:从应用层来看,只要把应用层报文交给下面的运输层,运输层就可以把这个报文传送给对方的运输层(哪怕双方相距很远,例如几千公里),好像这种通信就是沿着水平方向直接传送数据给对端的。之所以有这样的错觉,是因为运输层向上面的应用层屏蔽了下层网络核心的通信细节,下层的通信过程对应用层来说是透明的,它使得应用进程看见的好像就是在两个运输层实体之间有一条端到端的逻辑通信信道。

        TCP/UDP:这两个协议是运输层主要使用的协议。TCP:提供面向连接的、可靠的数据传输服务,其数据传输的单元是数据报(segment)。UDP:提供无连接的、尽最大努力交付的数据传输服务(不保证数据传输的可靠性),其数据传输的单元是用户数据报。

        复用/分用:运输层有复用和分用功能。复用就是多个应用层进程可同时使用下面运输层的传输服务;分用是运输层把收到的信息可以分别交付给上面应用层中的不同进程。

  • 网络层(network layer)

        网络层负责为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组或数据报进行传送。在TCP/IP 体系中,由于网络层使用IP协议,因此分组也叫做IP数据报,或简称为数据报。

        功能:为主机之间提供逻辑通信服务。具体来讲,就是解决数据传输过程中的路由选择问题,使源主机运输层传下来的分组,能够通过网络中的路由器找到目的主机。

        数据传输单元:IP数据报(IP分组)

请注意:不要将运输层的“用户数据报 UDP” 和 网络层的“IP数据报” 弄混。此外,无论在哪一层传送的数据单元,都可笼统地用“分组”来表示。

        IP(Internet Protocol)协议:它是用在网络层上的协议,也是 TCP/IP 协议族体系中最重要的协议之一,因此网络层也叫IP层。IP协议是一种面向消息的、不可靠传输的协议。与IP协议配套使用的还有三个协议:

  • ARP(Address Resolution Protocol,地址解析协议)
  • ICMG(Internet Control Message Protocol,网际控制报文协议)
  • IGMP(Internet Group Management Protocol,网际组管理协议)

网络层协议除了上面提到的4种主要协议外,还包括众多的路由选择协议。

        网络层通信特点:网络层向上只提供简单灵活、无连接的、尽最大努力交付的数据报服务。即,网络层不提供服务质量的承诺。也就是说,所传送的分组可能出错、丢失、重复和失序(即不按序到达终点),当然也不保证分组交付的时限。

        路由器(Router):它是网络层中使用的中间设备,它的作用是将不同的网络互相连接起来。

  • 数据链路层(data link layer)

数据链路层常简称为链路层

功能:数据链路层是为网络层服务的,解决的两个相邻网络结点之间的数据传输问题。

数据传输单元:数据帧(Frame)

局域网,它虽然也是一个网络,但是我们一般不把局域网放在网络层中讨论,这是因为在网络层要讨论的问题是多个网络互连的问题,是讨论一个IP分组怎样从一个网络,通过路由器,转发到另一个网络。当我们研究的是在同一个局域网中,数据是怎样从一台主机传送到另一台主机,但并不经过路由器转发。从整个互联网来看,局域网仍属于数据链路层的范围。我们经常听到的以太网,就是属于一种局域网,它是一种总线型拓扑结构的局域网。

  • 物理层(physical layer)

物理层,即在物理链路上进行数据传输,所传的数据就是二进制的比特 0 或 1。

数据传输单元:二进制比特

功能:解决怎样才能在连接各种计算机的传输媒体上传输数据比特流的问题,而不是指具体的传输媒体。可以将物理层的功能描述为确定计算机与传输媒体的接口有关的一些特性,包括:机械、电气、功能、过程特性。

参考链接

《计算机网络(第7版-谢希仁)》第1~6章

网络互联参考模型(详解)

1.2 TCP/UDP协议

        网络层(也叫IP层)解决数据传输过程中的路径选择问题。而运输层是以IP层提供的路径信息为基础完成实际的数据传输,使用的传输协议主要是TCP 和 UDP协议。这两个是运输层很重要的协议,务必弄明白。

关于TCP协议和UDP协议的详细内容,请参见下面的参考链接。

TCP协议-TCP连接管理

UDP协议详解

问题:运输层在 TCP/IP 协议栈中的地位和作用?为什么运输层是必不可少的?运输层的通信和网络层的通信有什么重要的区别?

答:从通信和信息处理的角度看,运输层向它上面的应用层提供通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最底层。当网络的边缘部分中的两个主机使用网络的核心部分的功能进行端到端的通信时,只有主机的协议栈才有运输层,而网络的核心部分中的路由器在转发IP数据报(IP分组)时都只用到下三层的功能。

        从网络层来看,通信的两端是两个主机,IP数据报的首部明确地标志了这两个主机的IP地址。但“两个主机之间的通信”这种说法还不够清楚。这是因为,真正进行通信的实体是在主机中的进程,是两个主机中的进程在交换数据(即通信)。因此严格地讲,两个主机进行通信就是两个主机中的应用进程在互相通信。IP协议虽然能把分组送到目的主机,但是这个分组还停留在主机的网络层而没有交付主机中的应用进程。从运输层的角度看,通信的真正端点并不是主机而是主机中的进程。也就是说,端到端的通信是应用进程之间的通信(如下图1-1所示)。因此,运输层是必不可少的。

图1-1  运输层为互相通信的应用进程提供了逻辑通信

        运输层的通信和网络层的通信有很大的区别。网络层提供主机之间的逻辑通信,而运输层则提供应用进程之间的逻辑通信。运输层还有复用、分用的功能,还要对收到的报文进行差错检测。

二 实现基于 TCP 的服务器端/客户端

2.1 TCP服务器的默认函数调用顺序

下图给出了TCP服务器默认的函数调用顺序,绝大部分TCP服务器都按照该顺序调用。

图2-2  TCP服务器端函数调用顺序

         调用socket()函数创建TCP套接字,声明并初始化服务器端的网络地址信息结构体变量,调用bind()函数向套接字分配地址。这两个阶段前面的文章已经讲过,下面讲解之后的几个过程。

2.2 进入等待连接请求状态

        当我们调用bind()函数给套接字分配了网络地址后,接下来就要通过调用listen()函数进入等待连接请求状态。只有调用了listen()函数,客户端才能进入可发出连接请求的状态。换言之,这时客户端才能调用 connect() 函数(若提前调用将发生错误),这也是为什么我们要先执行服务器端程序,后执行客户端程序的原因。

  • listen() — 将套接字设置为监听状态,即监听可能到来的连接请求。
#include <sys/socket.h>

int listen(int sockfd, int backlog);

//参数说明
//sockfd: 希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(即监听套接字)
//backlog: 最大连接请求等待队列(Queue)的长度,若为5,则等待队列长度为5,表示最多可以让5个连接请求进入等待队列

//返回值: 成功时返回0,失败时返回-1

        先解释一下等待连接请求状态和含义和连接请求等待队列。“服务器端处于等待连接请求状态” 是指,客户端请求建立TCP连接时,受理连接请求前一直使请求处于等待状态。下图给出了这个过程。

图2-3  等待连接请求状态

         由上图2-3克制,作为listen()函数的第一个参数传递的文件描述符套接字的用途。客户端连接请求本身也是从网络中接收到的一种数据,而要想接收就需要套接字。此任务就由服务器端套接字完成。服务器端套接字是接收连接请求的一名门卫或一扇门。

        客户端如果向服务器端询问:“请问我是否可以发起连接”?服务器端套接字就会亲切应答:“您好!当然可以,但系统正忙,请到等候室排号等待,准备好后会立即受理您的连接”。同时将连接请求请到等候室。调用listen()函数即可生成这种门卫(服务器端套接字),listen函数的第二个参数将决定了等候室的大小。等候室称为连接请求队列等待队列,准备好服务器端套接字和连接请求等待队列后,这种可接收连接请求的状态称为等待连接请求状态

        listen()函数的第二个参数值与服务器端的特性有关,像频繁接收连接请求的Web服务器端至少应为15。另外,连接请求等待队列的大小始终要根据实际使用情况而定。

2.3 受理客户端连接请求

        调用listen()函数后,若有新的连接请求,则应按顺序受理。受理连接请求意味着进入可接收数据的状态。接收数据需要使用的部件当然是套接字了,可能有人也许会认为我们可以使用服务器端套接字,但是服务器端套接字是做门卫的。如果在与客户端的数据交换中使用门卫,那谁来守门呢?因此需要另外一个套接字,但没必要亲自创建。我们是使用 accept() 函数就会自动创建一个新的套接字,并连接到发起请求的客户端。

  • accept() — 服务器端受理客户端连接请求
#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

//参数说明
//sockfd: 套接字对应的文件描述符
//addr: 保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量实参填充客户端地址信息
//addrlen: 第2个参数addr结构体的长度,指向存有长度变量的地址。函数调用后,该变量即被填入客户端地址长度

//返回值: 成功时返回0,失败时返回-1

        accept()函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功后,accept()函数内部将产生用于数据I/O的新套接字,并返回其文件描述符。需要强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。下图展示了accept()函数调用过程。

图2-4  受理连接请求状态

         上图2-4 展示了 “从连接请求等待队列中取出1个连接请求,创建一个新套接字并完成连接请求” 的过程。服务器端单独创建的套接字与客户端建立连接后进行数据交换。

2.4 客户端的默认函数调用顺序

        客户端的函数调用顺序要比服务器端简单许多。因为创建套接字和请求连接就是客户端的全部内容。如下图所示。

图2-5  TCP客户端函数调用顺序

         与服务器端相比,区别就在于“请求连接”,它是创建客户端套接字后向服务器端发起的连接请求。服务器端调用listen()函数后创建连接请求等待队列,之后客户端即可发起请求连接。客户端发起请求连接时使用connect()函数来完成的。

  • connect() — 客户端向服务器端发起连接请求
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

//参数说明
//sockfd: 套接字对应的文件描述符
//serv_addr: 保存目标服务器端网络地址信息的结构体变量的地址值
//addrlen: 以字节为单位传递已传递给第2个结构体参数serv_addr的地址变量长度

//返回值: 成功时返回0,失败时返回-1

《函数说明》客户端调用 connect 函数后,发生以下情况之一才会返回(完成函数调用)

  • 服务器端接收了连接请求。
  • 发生断网等异常情况而中断连接请求。

        需要注意的是,所谓“接收连接”并不意味着服务器端调用了accept函数,其实是服务器端把连接请求信息记录到等待队列。因此connect函数返回后,并不会立即进行数据交换。

《知识拓展》客户端套接字地址信息在哪?

        实现服务器端程序必经过程就是给套接字分配IP地址和端口号。但客户端实现过程中并未出现套接字地址分配过程,而是创建套接字后立即调用connect()函数。难道客户端套接字无需分配IP地址和端口号?当然不是!网络数据交换必须分配IP地址和端口号。既然如此,那客户端套接字何时、何地、如何分配网络地址呢?

  • 何时?调用connect()函数时。
  • 何地?操作系统,更准确地说是是内核中。
  • 如何?IP用客户端主机的IP,端口号由操作系统随机分配。

        客户端的IP地址和端口号在调用connect()函数时由操作系统自动分配,无需调用标记的bind函数进行分配。当然,客户端也是可以使用bind()函数自己主动分配指定的IP地址和端口号的,但是在客户端程序一般都不会这么做的,因为不需要。

2.5 基于TCP的服务器端/客户端函数调用关系

        前面讲解了TCP服务器端/客户端的实现顺序,实际上二者并非相互独立,下图展示了服务器端/客户端之间的交互过程。

图2-6  函数调用关系

         图2-6 的总体流程整理如下:服务器端创建套接字后连续调用bind、listen函数进入等待连接请求状态,客户端通过调用connect函数发起连接请求。需要注意的是,客户端只能等到服务器端调用listen函数后才能调用connect函数。同时要清楚,客户端调用connect函数前,服务器端有可能已率先调用accept函数。当然,此时服务器端在调用accept函数时会进入阻塞(blocking)状态,直到客户端调用connect函数为止。

三 实现迭代服务器端/客户端

        接下来我们实现一个回声(echo)服务器端/客户端通信程序。顾名思义,服务器端将客户端传输过来的字符串数据原封不动地传回给客户端,就像回声一样。

3.1 实现迭代服务器端

        之前讨论的“Hello world”服务器端程序处理完1个客户端连接请求后即退出,连接请求等待队列实际没有多大意义。但这并非我们想象的服务器端。设置好等待队列的大小后,应向所有客户端提供服务。如果向继续受理后续的客户端连接请求,应怎样扩展代码呢?最简单的办法就是插入循环语句反复调用accept()函数,其函数调用过程如下图所示:

图3-7  迭代服务器端的函数调用顺序

         从上图3-7可以看出,调用accept()函数后,紧接着调用I/O相关的read、write函数,然后调用close函数。这并非针对服务器端套接字,而是针对accept()函数调用时创建的新套接字。

        调用close函数就意味着结束了针对某一客户端的服务。此时如果还想服务于其他客户端,就要重新调用accept()函数。我们不仅要问:“这算什么呀?又不是银行窗口,好歹也是一个服务器端,难道同一时刻只能服务于一个客户端吗?”

        是的!同一时刻确实只能服务于一个客户端。当然,我们可以使用多进程多线程的方式编写同时服务多个客户端的服务器端程序,目前我们这里暂不涉及。

3.2 迭代服务器端/客户端实现代码

        我们首先整理一下程序的基本运行逻辑。

  • 服务器端在同一时刻只与一个客户端连接,并提供回声服务。
  • 服务器端依次向5个客户端提供服务并退出。
  • 客户端接收用户输入的字符串并发送到服务器端。
  • 服务器端将接收到的字符串数据原封不动地传回给客户端,即“回声”。
  • 服务器端与客户端之间的字符串回声一直执行到客户端输入字符: “Q” 为止,然后断开TCP连接。

1、服务器端程序 echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char message[BUF_SIZE];
    int str_len, i;
    
    struct sockaddr_in serv_adr;    //服务器端地址信息变量
    struct sockaddr_in clnt_adr;    //客户端地址信息变量
    socklen_t clnt_adr_sz;
    
    if(argc!=2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock==-1)
        error_handling("socket() error");
    
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
    
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
    
    clnt_adr_sz=sizeof(clnt_adr);

    for(i=0; i<5; i++)  //为处理5个客户端连接而添加的循环语句,共调用5次accept函数,依次向5个客户端提供服务
    {
        clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);  //受理客户端连接请求,并返回新的套接字文件描述符
        if(clnt_sock==-1)
            error_handling("accept() error");
        else
            printf("Connected client %d\n", i+1);
    
        while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)  //接收客户端发来的消息
            write(clnt_sock, message, str_len);                 //回送客户端发来的消息

        close(clnt_sock);                                       //关闭与客户端进行数据交互的套接字
    }

    close(serv_sock);                                           //关闭服务器端的监听套接字
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

 编译程序:gcc echo_server.c -o eserver

运行程序:./eserver 9190

Connected client 1

Connected client 2

Connected client 3

1、客户端程序 echo_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;

    if(argc!=3) {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    
    sock=socket(PF_INET, SOCK_STREAM, 0);    //创建客户端TCP套接字
    if(sock==-1)
        error_handling("socket() error");
    
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
    
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)  //调用connect函数,向服务器端发起连接请求
        error_handling("connect() error!");
    else
        puts("Connected...........");
    
    while(1) 
    {
        fputs("Input message(Q to quit): ", stdout);          //标准输出
        fgets(message, BUF_SIZE, stdin);                      //标准输入
        
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))  //如果输入字符q或Q,则退出循环体
            break;

        write(sock, message, strlen(message));                //向服务器端发送字符串消息
        str_len=read(sock, message, BUF_SIZE-1);              //接收来自服务器端的消息
        message[str_len]= '\0';                               //在字符数组尾部添加字符串结束符'\0'
        printf("Message from server: %s", message);           //输出接收到的消息字符串
    }
    
    close(sock);                                              //关闭客户端套接字
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

编译程序:gcc echo_client.c -o eclient

运行程序:./eclient

Connected...........

Input message(Q to quit): Good morning

Message from server: Good morning

Input message(Q to quit): Hi

Message from server: Hi

Input message(Q to quit): Q

《程序说明》我们编写的回声服务器端/客户端程序是以字符串为单位传递数据。理解了这一点后再观察echo_client.c程序的下面两行代码:

write(sock, message, strlen(message));

str_len=read(sock, message, BUF_SIZE-1);

我们知道,TCP协议的特点之一就是基于字节流的,没有数据边界的,因此我们应该要意识到这两行代码是不太适合做字符串单位的回声的。

3.3 回声客户端存在的问题

下面是 echo_client.c 程序的几行代码:

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = '\0';
printf("Message from server: %s", message);

        以上代码有个错误假设:“每次调用read、write函数时都会以字符串单位执行实际的I/O操作。”

        当然,每次调用write函数都会传递1个字符串,因此这种假设在某种程度上也算合理。但是我们知道TCP协议的特点之一就是传输的数据不存在数据边界。因此,多次调用write函数传递的字符串有可能一次性传递到服务器端。此时客户端有可能从服务器端收到多个字符串,这不是我们希望看到的结果。还需要考虑服务器端的如下情况:

“字符串太长,需要分成2个TCP报文段发送!”

        服务器端希望通过调用1次write函数传输数据,但如果数据太大,超过了TCP最大报文段长度MSS,操作系统就会把数据分成多个TCP报文段发送给客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用了read函数。

        所有这些问题都源自于TCP协议的数据传输特性。那该如何解决呢?我们将在下一篇博文中解释说明。

        虽然我们编写的服务器端/客户端回声程序运行的结果是正确的。但是这只是运气好罢了!只是因为收发的数据量小,而且运行环境为同一台主机或相邻的两台主机,所以没有发生错误,可实际上仍存在发生错误的可能。

四 习题

1、请输出TCP/IP协议栈中链路层和IP层的作用,并给出二者的关系。

:链路网是WAN(广域网)、MAN(城域网)、LAN(局域网)等网络标准相关的协议栈,是定义物理特性标准的层级。IP层是定义网络传输数据标准的层级。

二者的关系是:IP层数据报是在链路层的基础上传输数据的。链路层负责物理结点的连接,IP层负责为数据传输选择合适的传输路径。

2、为何需要把TCP/IP协议栈分成4层(或7层)?结合开放式系统回答

答:将极其复杂的TCP/IP协议栈分层设计,可以简化协议的设计难度,将一个大问题划分成若干个小问题,再逐个攻破,将大幅提供设计效率。

更重要的原因的是,TCP/IP协议栈属于开放式系统,为了通过标准化操作设计开放式系统,所以采用分层设计的思想。按照不同层级要求,制定了统一的层级标准,从而使不同体系结构的计算机网络都能互联互通。这种标准化设计是TCP/IP蓬勃发张的重要原因。

3、客户端调用connect函数向服务器端发送连接请求。服务器端调用哪个函数后,客户端可以调用connect函数?

:服务器端调用了listen函数后,客户端才可以调用connect函数。

4、什么时候创建连接请求等待队列?它有何作用?与accept函数有什么关系?

:服务器端调用listen函数时创建了连接请求等待队列。它是用来临时存放客户端的连接请求信息的。accept函数调用后,服务器端套接字开始受理连接请求,它将从连接请求等待队列的队头依次取出客户端的连接请求信息,并与客户端建立连接。

5、客户端中为何不需要调用bind函数分配地址?如果不调用bind函数,那何时、如何向套接字分配IP地址和端口号?

:因为客户端套接字是连接请求的发起方,并不需要主动监听自己的的网络地址信息,只需要将其告诉通信对端即可,因此没必要通过bind函数明确地分配地址信息。

客户端在调用connect函数时,操作系统会自动为客户端套接字分配IP地址和端口号,并与之绑定在一起。

参考

《TCP-IP网络编程(尹圣雨)》第4章 - 基于TCP的服务器端/客户端(1)

《计算机网络(第7版-谢希仁)》第1-6章

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux网络编程 - 基于TCP的服务器端/客户端(1) 的相关文章

  • TCP/IP网络编程(一)

    TCP IP网络编程 第一章 理解网络编程和套接字 实现 先准备一个linux的操作系统然后用xshell连上 CentOS7安装以及Xshell连接常见问题解决 jump into zehe的博客 CSDN博客 下载gcc root lo
  • http请求学习

    GET 向Web服务器请求一个文件 POST 向Web服务器发送数据让Web服务器进行处理 PUT 向Web服务器发送数据并存储在Web服务器内部 HEAD 检查一个对象是否存在 DELETE 从Web服务器上删除一个文件 CONNECT
  • 使用Go自己动手搭建一个HTTP代理服务器

    代理服务器的工作方式如下 客户端向代理服务器发送请求 表明自己需要请求的网站内容 代理服务器接收到来自客户端的请求之后 通过解析 获取到需要访问的web服务 代理服务器将客户端的请求信息全部转发给web服务器 web服务器返回响应消息给代理
  • valgrind:内存泄漏的检查工具

    valgrind 是帮助程序员寻找程序里的 bug 和改进程序性能的工具集 擅长发现内存的管理问题 里面有若干工具 其中最重要的是 memcheck 工具 用于检查内存的泄漏 memcheck 能发现如下的问题 使用未初始化的内存 使用已经
  • 认识传输层(UDP与TCP)

    传输层主要负责数据能够从发送端发送到接收端 要正确传输就要明确发送端和接收端 这时候IP地址和端口号一起就可以确定一端了 那么他们是怎么唯一标识的呢 1 端口号 port 端口号唯一标识一个主机上进行通信的不同应用程序 在TCP IP协议中
  • libevent库学习(1)

    一 初识 1 libevent介绍 Libevent 是一个用C语言编写的 轻量级的开源高性能事件通知库 主要有以下几个亮点 事件驱动 event driven 高性能 轻量级 专注于网络 不如 ACE 那么臃肿庞大 源代码相当精炼 易读
  • IP协议详解之IPv6头部结构简介

    IPv6协议是网络层技术发展的必然趋势 因为它不仅解决了IPv4地址不够用的情况 还做了很大的改进 比如 增加了多播和流的功能 为网络上多媒体内容的质量提供精细的控制 引入自动配置功能 使得局域网管理更加方便 增加了专门的网络安全功能 IP
  • 网络传输的基本流程

    1 网络传输的进本流程 同一网段内两台主机进行文件传输 文件传输的流程 2 理解封装和分用 不同协议对数据报有不同的称谓 在传输层叫做段 segment 在网络层叫做数据报 datagram 在链路层叫做帧 frame 应用层数据通过协议栈
  • 05libevent库下未决与非未决的解释

    05libevent库下未决与非未决的解释 以下是关于libevent学习的相关文章 01libevent库的下载与安装并且测试是否安装成功 02libevent库的整体框架思想 03libevent下通信的主要函数 04libevent库
  • 七、网络编程之同步非阻塞式网络 IO 模型详解

    上文中探讨了同步阻塞式网络 IO 模型 本文将讨论另一种模型 同步非阻塞 IO 模型 同步非阻塞式网络 IO 模型详解 同步非阻塞 IO 原理 同步非阻塞 IO non blocking IO 应用进程发起 IO 系统调用后 内核立即返回给
  • Linux网络编程socket错误分析

    转自 http aigo iteye com blog 1911134 socket错误码 EINTR 4 阻塞的操作被取消阻塞的调用打断 如设置了发送接收超时 就会遇到这种错误 只能针对阻塞模式的socket 读 写阻塞的socket时
  • moudo网络库剖析

    muduo简介 muduo是陈硕大神在Linux平台下基于C C 开发的高性能网络库 在此基础上可以很方便的扩展 进行二次开发编写如http服务器 muduo网络库的核心框架 one thread per thread Reactor模式
  • 设置可执行程序的名称

    argc 命令行参数的个数 argv 是个数组 每个数组元素都是指向一个字符串的 char 里边存储的内容是所有命令行参数 argv 内存之后接着就是连续的环境变量参数信息内存 里边存储的内容是可执行程序执行时有关的所有环境变量参数信息 可
  • 关于socket的各种错误码

    1 INVALID SOCKET 表示该 socket fd 无效 如 accept 2 或 socket 2 等在创建socketfd时 int m socket socket AF INET SOCK STREAM 0 if m soc
  • unix域套接字

    UNIX域套接字被用来和同一机器上运行的进程通信 尽管因特网域套接字可以用作同样的目的 然而UNIX域套接字更高效 UNIX域套接字只拷贝 数据 它们没有要执行的协议处理 没有要增加或删除的网络头 没有要计算的校验和 没有要产生的序列号 没
  • 三、Linux网络编程:Socket编程-网络模型

    3 Socket编程 网络模型 3 1 OSI七层模型 图解 每层的功能 模型 功能 物理层 比特流传输 数据链路层 网络控制 链路纠错 网络层 寻址 路由 传输层 建立主机端到端的连接 会话层 建立 维护和管理会话 表示层 格式转化 加密
  • 关于AF_INET和PF_INET

    AF 表示ADDRESS FAMILY 地址族 PF 表示PROTOCL FAMILY 协议族 Winsock2 h中 define AF INET 0 define PF INET AF INET 所以在windows中AF INET与P
  • java socket聊天室 swing做界面 Tcp为通讯协议 支持私聊 群聊 发文件

    Java的的的的聊天室 源代码下载 首先我们来看看程序界面 丑到爆 勉强能用就行啦 第一个 登录界面 第二个 用户界面 第三个 服务器界面 好了上面三个界面是程序的主界面 下面我们先讲讲如何使用源代码 使用条件 一数据库 我这里用的MyS
  • 日志 - 客户端及服务端写法

    一 客户端 先来看一个日志类的实现方法 这个日志类也是代表着大多数客户端日志的主流写法 log h 1 ifndef LOG H 2 define LOG H 3 4 include
  • 03libevent下通信的主要函数

    03libevent下通信的主要函数 以下是关于libevent学习的相关文章 01libevent库的下载与安装并且测试是否安装成功 02libevent库的整体框架思想 03libevent下通信的主要函数 04libevent库下fi

随机推荐

  • 关于汇编语言寄存器和指令操作的整理

    最近汇编学到后面的内容 越来越觉得前面的基础没有掌握好 弄得最后编写汇编程序的时候 寄存器瞎用 没有一点的规矩 中断操作也不知道是对哪个寄存器里的数进行操作 每次做一个小程序 都得翻书后面的INT中断查询表 感觉很不爽 今天花了大半天把几本
  • 数据通信——因特网基础

    引言 之前最开始并接触学习的是华为的路由器交换机等知识 后来慢慢的扩充到了计算机网络 现在在备考计网专业课 因此写一下我对计网现阶段知识的认知 也是对考研备考时的一个复习 我将用易于理解的逻辑阐述下计网中难以理解的知识 希望大家也多多补充并
  • Visual C++ 运行窗口一闪而过的解决方法

    本文简单介绍了Visual C 编写运行程序的步骤 解决Visual C 2010开发环境中运行程序时运行窗口一闪而过的问题 总结了 断点调试 F5 方法 开始执行 Ctrl F5 方法 调试工具条方法 暂停语句或者输入等待方法 共4种方法
  • jvisualvm远程监控centos上虚拟机的状态

    要使用jvisualvm监视远程主机需要先再远程主机上运行jstatd 1 在 JAVA HOME bin目录下新建jstatd all policy文件 内容如下 grant codebase file java home lib too
  • 符号优先级误区一:移位运算符优先级比加减运算符低

    目录 移位运算符优先级比加减运算符低 误区 发现问题 实际结果 举例子证实 代码如下 实际结果 移位运算符优先级比加减运算符低 这是我在做二分查找时发现的问题 误区 发现问题 直接除2 int mid start end start 2 移
  • message from server: “Host is not allowed to connect to this MySQL server“问题的解决办法

    数据库安装完成后 默认是不能远程登陆的 只能在本地用localhost 或者127 0 0 1登录访问 如果需要远程登录 则需要修改mysql设置 具体修改方式 1 本地登录mysql root localhost mysql u root
  • Java的网络编程

    网络编程是指编写运行在多个设备 计算机 的程序 这些设备都通过网络连接起来 java net 包中 J2SE 的 API 包含有类和接口 它们提供低层次的通信细节 你可以直接使用这些类和接口 来专注于解决问题 而不用关注通信细节 java
  • react生命周期---参考学习20220726

    react生命周期笔记 笔记 react生命周期 链接 react生命周期 https projects wojtekmaj pl react lifecycle methods diagram 1 挂载时 挂载时调用四个生命周期函数 2
  • Nginx高可用实战

    来源 jingfengjiaoyu Nginx的特点 跨平台 Nginx 可以在大多数 Unix like OS编译运行 而且也有Windows的移植版本 配置异常简单 非常容易上手 配置风格跟程序开发一样 神一般的配置 非阻塞 高并发连接
  • [QT编程系列-42]: QT定时器

    目录 第1章 QT下的定时器 1 1 主要的对象 1 2 QTTimer定时的特点 1 3 QT高精度定时器QElapsedTimer 计时 第2章 Windows操作系统下的高性能定时器 2 1 时钟分辨率 2 2 两种来实现高性能定时器
  • 使用阿里云日志服务来分析日志

    随着云服务技术越来越成熟 作为一枚运维 不得不感慨云计算的发展对我的职业生涯起了积极推动的作用 一方面我可以通过云服务来提高我的工作效率 另一方面我节省了更多时间来学习 在提高我专业度的同时 个人能力也越来越强 在此我就以阿里云日志服务 给
  • μCOS-II系统之时间管理函数OSTimeDlyHMSM()

    上次学习了OSTimeDly 函数 了解了OSTimeDly 基本应用 同时 COS II还提供了另一个系统延时函数OSTimeDlyHMSM 函数 下面来说说这个函数的基本应用 这个函数是以小时 H 分 M 秒 S 和毫秒 m 四个参数来
  • 二分查找算法c语言函数,BinarySearch 经典二分查找算法

    前言 二分查找算是最经典也最入门的算法了 大一新生刚学C语言就开始写 但是看似简单的二分算法 想要考虑周全写得完美也是要费点功夫 比如java库里的二分查找 一个溢出的bug还存在了很久 更有号称90 程序员写不出无BUG的二分查找程序 夸
  • 亲测有效!帮你更方便更舒服使用ubuntu20.04!!!

    今天要记录的是如何更舒服的使用ubuntu20 04 全部内容就在上面这张图里 包括三方面 1 ubuntu美化 2 ubuntu扩展 3 必备软件 1 ubuntu美化 这部分内容可以直接参考 这位大佬 讲的很详细也很清楚 需要做一点补充
  • 【JVM 内存结构

    内存结构 前言 简介 程序计数器 定义 作用 特点 示例 应用场景 主页传送门 传送 前言 Java 虚拟机的内存空间由 堆 栈 方法区 程序计数器和本地方法栈五部分组成 简介 JVM Java Virtual Machine 内存结构包括
  • 异常相关面试题

    1 java中的异常继承体系及常见运行时异常 Throwable类是所有异常或错误的超类 它有两个子类 Error和Exception 分别表示错误和异常 其中异常Exception 分为运行时异常 RuntimeException 和编译
  • js合并数组对象(将数组中具有相同属性对象合并到一起,组成一个新的数组)

    一 根据数组对象中某一key值 合并相同key值的字段 到同一个数组对象中 组成新的数组 1 原数组 var array id 1 name Alice id 2 name Bob id 1 age 25 id 3 name Charlie
  • 机器学习 | Sklearn中的朴素贝叶斯全解

    前期文章介绍了朴素贝叶斯理论 掌握理论后如何去使用它 是数据挖掘工作者需要掌握的实操技能 下面来看看Sklearn中都有哪些朴素贝叶斯 朴素贝叶斯是运用训练数据学习联合概率分布 及
  • uniapp微信小程序实现对H5的全屏适配(@莫成尘)

    复制代码您将看到和一下截图一样的效果 我们将适配全屏至正常h5下的所以页面大小 您再此处将依然使用rpx作为开发单位
  • Linux网络编程 - 基于TCP的服务器端/客户端(1)

    一 理解 TCP 和 UDP 根据数据传输方式的不同 基于网络传输协议的套接字一般分为TCP套接字和UDP套接字 因为TCP是面向连接的 因此又称为基于流 stream 的套接字 TCP Transmission Control Proto