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

前言

         在上一篇博文中,我们实现了回声服务器端/客户端程序,并且还遗留一个回声客户端程序存在的一个问题。在本篇博文中,我们将给出遗留问题的解决方案,并详细讨论TCP的工作原理。

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

一 回声客户端的完美实现

1.1 回声服务器端没有问题,只有回声客户端有问题?

问题不在服务器端,而在客户端。但只看代码也许不太好理解,因为I/O中使用了相同的函数。先回顾一下回声服务器端的I/O习惯代码,下面是 echo_server.c 的I/O操作相关代码:

while((str_len=read(clnt_sock, message, BUF_SIZE)) !=0 )
    write(clnt_sock, message, str_len);

        接着回顾回声客户端代码,下面是 echo_client.c 的I/O操作相关代码:

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

        二者都是循环调用read或write函数。实际上之前的回声客户端将100%接收自己传输的数据,只不过接收数据时的单位有些问题。扩展客户端代码,下面是 echo_client.c 收发数据的I/O操作代码:

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);           //输出接收到的消息字符串
}

        从上面的代码可以看出,回声服务器传输的是字符串,而且是通过调用write函数一次性发送的。之后还调用一次read函数,期待着接收自己传输的字符串。这就是问题所在

        “既然回声客户端会收到所有字符串数据,是否只需多等一会儿?过一段时间后再调用read函数是否可以一次性读取所有字符串数据呢?”

        的确,过一段时间后即可接收,但需要等多久?要等10分钟吗?这不符合常理,理想的客户端应在收到字符串数据时立即读取并输出。

        这里我们有必要对read()函数的返回值情况做一下说明:

//read 函数原型
ssize_t read(int fd, void *buf, size_t count);

1、如果读取成功,则返回实际读到的字节数。这里又有两种情况:一是如果在读完count要求字节之前已经到达文件的末尾,那么实际返回的字节数将小于count值,但是仍然大于0;二是在读完count要求字节之前,仍然没有到达文件的末尾,这时实际返回的字节数等于要求的count值。
2、如果读取时已经到达文件的末尾,则返回0。
3、如果出错,则返回1。
这样也就是说分为>0、<0、=0三种情况进行讨论。在有的时候,<0、=0可以合为一种情况进行处理。这要根据程序的实际情况进行处理。

1.2 回声客户端问题解决方法

        解决的办法是使用循环,每次调用read函数就立即返回实际接收到的数据大小。如果实际接收到的数据量小于期望收到的,则循环调用read函数,直至接收到所有的数据。因此,我们可以修改一下 echo_client.c 代码,修改后的代码如下:

  • echo_client2.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;     //新增两个整型变量recv_len,recv_cnt
    struct sockaddr_in serv_adr;

    if(argc!=3) {
        printf("Usage: %s  \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;

        str_len = write(sock, message, strlen(message));      //向服务器端发送字符串消息,并返回发送的数据量大小
        
        //@override
        recv_len = 0;                                               //初始化接收数据量大小变量
        while(recv_len < str_len)
        {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1);  //接收来自服务器端的消息,并返回实际接收到的数据量
            if(recv_cnt == -1)                                      //如果接收出错,打印错误信息
                error_handling("read() error!");
            recv_len += recv_cnt;                                   //累加接收到的数据量 
        }
        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);
}

        在 echo_client.c 代码中只调用了一次read函数,而在echo_client2.c 代码中为了接收所有传输数据而循环调用了read函数,while循环条件是 while(recv_len < str_len),也可以写成:while(recv_len != str_len) 的形式,但是更推荐前面使用前一种循环条件。因为假设发生异常情况,读取数据过程中 recv_len 超过了 str_len,此时就无法退出循环,有可能引发无限循环。而如果while循环条件是第一种形式,则即使发生异常也不会陷入无限循环。写循环语句时应尽量降低因异常情况而陷入无限循环的可能

1.3 如果问题不在于回声客户端:定义应用层协议

        回声客户端可以提前知道接收的数据长度,但我们需要意识到的是,在更多情况下是无法预先知道要接收的数据长度的。既然如此,若无法预知接收数据长度时应如何收发数据呢?此时需要的就是应用层协议的定义。之前的回声服务器端/客户端程序中定义了如下协议:

收到Q或q就立即终止TCP连接。

        同样,收发数据过程中也需要定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。可以看出,应用层协议并不是高深莫测,只不过是为特定程序的实现而制定的规则。

1.4 实现计算器服务器端/客户端程序

        下面编写一个示例程序以体验应用层协议的定义过程。该程序中,服务器端从客户端获得多个数字和运算符信息。服务器端收到数字后对其进行加减乘运算,然后把计算结果传回客户端。例如,向服务器端传递3、5、9的同时请求加法运算,则客户端收到 3+5+9 的运算结果;若请求做乘法运算,则客户端收到 3×5×9 的运算结果。而如果向服务器传递4、3、2 的同时要求做减法,则客户端将收到 4-3-2 的运算结果,即第一个参数成为被减少。

        在编写程序之前,我们需要先设计一下应用层协议。为了简单起见,我们只设计了最低标准的协议,在实际的应用程序实现中需要的协议更详细、更准确。应用层协议规则定义如下:

  • 客户端连接到服务器端后以1字节整数形式传递待运算数字个数。
  • 客户端向服务器端传递的每个整数型数据占用4字节。
  • 传递整数型数据后接着传递运算符。运算符信息占用1字节。
  • 选择字符 +、-、* 之一传递。
  • 服务器端以4字节整数型向客户端传回运算结果。
  • 客户端得到运算结果后终止与服务器端的连接。

        这种程度的协议相当于实现了一半程序,这也说明应用层协议设计在网络编程中的重要性。只要设计好协议,实现程序就不会成为大问题。另外要记住的一点,调用close()函数将向通信对端传递 EOF,请各位记住这一点并加以运用。

        我们先实现计算器客户端程序代码。

  • 计算器客户端程序 op_client.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define OPSZ     4      //操作数占用字节数
#define RLT_SIZE 4      //运算结果数占用字节数

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    char opmsg[BUF_SIZE] = {0};
    int result, opnd_cnt, i;
    struct sockaddr_in serv_addr;
    
    if(argc != 3){
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }
    
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));
    
    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!")
    else
        puts("Connected...........");

    fputs("Operand count: ", stdout);
    scanf("%d", &opnd_cnt);             //输入操作数个数
    opmsg[0] = (char)opnd_cnt;          //将操作符个数存入字符数组,占用1个字节
    
    for(i=0; i

我们给出客户端向服务器端传送的数据的数据格式示例,如下图所示:

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第1张图片 图1-1  客户端op_client.c的数据传送格式

从图1-1 中可以看出,若想在同一数组中保存并传输多种数据结构,应把数组声明为char类型。而且需要额外做一些指针及数组运算。

        接下来我们实现服务器端的程序代码,如下所示:

  • 计算器服务器端程序 op_server.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE 1024
#define OPSZ     4      //操作数占用字节数

void error_handling(char *message);
int calculate(int opnum, int opnds[], char operator);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char opinfo[BUF_SIZE] = {0};
    int result, opnd_cnt;
    int recv_cnt, recv_len;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_sz;
    
    if(argc != 2){
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }
    
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error!");
    
    memset(serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error!");
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error!")
    
    clnt_addr_sz = sizeof(clnt_addr);
    for(i=0; i<5; i++)
    {
        opnd_cnt = 0;
        clnt_sock = accept(serv_addr, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
        read(clnt_sock, &opnd_cnt, 1);       //读取1字节操作数个数,存入opnd_cnt变量中
        
        recv_len = 0;
        while(opnd_cnt*OPSZ+1 > recv_len)    //循环读取剩余的数据
        {
            recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE);
            recv_len += recv_cnt;
        }
        result = calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]);
        write(clnt_sock, (char*)&result, sizeof(result));   //向客户端传回运算结果消息
        close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

int calculate(int opnum, int opnds[], char op)
{
    int result = opnds[0], i;
    swith(op)
    {
    case '+':
        for(i=1; i
  • 运行结果
  • 服务器端

编译程序:gcc op_server.c -o opserver

运行程序:./opserver 9190

  • 客户端

编译程序:gcc op_client.c -o opclient

运行结果1:./opclient 127.0.0.1 9190

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

Operand count: 3

Operand 1: 12

Operand 2: 24

Operand 3: 36

Operator: +

Operation result: 72

运行结果2:./opclient 127.0.0.1 9190

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

Operand count: 2

Operand 1: 24

Operand 2: 12

Operator: -

Operation result: 12

《结果分析》从运行结果可以看出,客户端首先询问用户待输入数字的个数,再输入相应个数的整数,最后以运算符的形式输入符号信息,并输出运算结果(+、-、* 之一)。当然,实际的运算操作是由服务器端做的,客户端只是接收运算结果并输出给用户。

二 TCP原理

2.1 TCP套接字中的I/O缓冲

        我们已经知道,TCP套接字的数据收发无边界。服务器端即使调用1次write函数传输40字节的数据,客户端也有可能通过调用4次read函数每次读取10字节。但此处也有一些疑问,服务器端一次性传输了40字节,而客户端居然可以缓慢地分批接收。客户端接收10字节后,剩下的30字节在何处等候呢?是不是像飞机为了等待着陆而在空中盘旋一样,剩下的30字节也在网络中徘徊并等等接收呢?

        实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,如下图所示,write函数调用瞬间,数据被移至输出缓冲区(即发送缓冲区);read函数调用瞬间,从输入缓冲区(即接收缓冲区)读取数据。

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第2张图片 图2-2  TCP套接字的I/O缓冲

         如上图2-2所示,调用write函数时,数据被移至输出缓冲,在适当的时候(不管是分别发送还是一次性发送)传向对端的输入缓冲。这是对方将调用read函数从输入缓冲读取数据。这些I/O缓冲特性可整理如下。

  • I/O缓冲在每个TCP套接字中单独存在。
  • I/O缓冲在创建套接字时自动生成。
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据。
  • 关闭套接字将丢失输入缓冲(即接收缓冲)中的数据。

        那么下面这种情况会引发什么事情?

“客户端输入缓冲(即接收缓冲)为50字节,而服务器传输了100字节。”

        这的确是一个问题。接收缓冲只有50字节,却收到了100字节的数据。可以提出如下解决方案:

“填满接收缓冲前迅速调用read函数读取数据,这样会腾出一部分空闲的缓冲空间,问题就解决了”

        但是TCP协议有流量控制机制,因此 “不会发生超过接收缓冲大小的数据传输”。

        也就是说,根本不会发生这类问题,TCP协议利用滑动窗口(Sliding Window)机制来实现流量控制。

        所谓流量控制(flow control)就是让发送发的发送速率不要太快,要让接收方来得及接收。用对话方式呈现如下:

  • 套接字A:“你好,最多可以向我传递50字节数据。”
  • 套接字B:“OK!”

  • 套接字A:“我腾出了20字节的空间,最多可以接收70字节数据。”
  • 套接字B:“OK!”

        数据收发也是如此,因此TCP中不会因为缓冲溢出而丢失数据。

《提示》从write函数返回的时间点

        write函数并不是在向通信对端传输完所有数据时才返回,而是在数据被移到TCP套接字的发送缓冲时就返回了。但TCP会保证对发送缓冲数据的传输,所以说write函数在数据传输完成时返回,我们要准确理解这句话的真正内涵。

2.2 TCP内部工作原理1:与对方套接字的连接

TCP套接字从创建到消失所经历过程分为如下3部。

  • 与对方套接字建立连接。
  • 与对方套接字进行数据交换。
  • 断开与对方套接字的连接。

我们首先讲解与对方套接字建立连接的过程。连接过程中套接字之间的对话如下:

  • [Shake 1] 套接字A:“你好,套接字B。我这儿有数据要传给你,建立连接吧。”
  • [Shake 2] 套接字B:“好的,我这边已就绪。”
  • [Shake 3] 套接字C:“谢谢你受理我的连接请求。”

        TCP在实际连接建立过程中也会经过3次对话过程。因此,该过程又称 “Three-way handshaking(三报文握手)”。接下来给出连接过程中实际交换的信息格式,如下图所示:

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第3张图片 图2-3  TCP套接字的连接建立过程

         TCP套接字是以全双工(Full-duplex)方式工作的。也就是说,它可以双向传递数据,即可接收,也可发送。因此,正式收发数据前需要做一些准备工作。

1、首先,请求连接的主机A向主机B传递如下信息:

[SYN]  SEQ: 1000,  ACK: -

该消息中 SEQ为1000,ACK为空,而SEQ为1000的含义是:“现传递的数据报的初始序号为1000,如果接收无误,请通知我向您传递1001号数据包。

这是首次请求连接时使用的消息,又称SYN(同步)。SYN 是 Synchronization 的简写,表示收发数据前传输的同步消息。

2、接下来主机B向主机A传递如下消息:

[SYN+ACK] SEQ: 2000,  ACK: 1001

此时SEQ为2000,ACK为1001,而SEQ为2000的含义是:“现传递的数据包初始序号为2000,如果接收无误,请通知我向您传递2001号数据包。

而ACK: 1001 的含义是:“刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包。

        对主机A首次传输的数据包的确认消息(ACK:1001)和为主机B传输数据做准备的同步消息(SEQ:2000)捆绑发送,因此,此种类型的消息又称为 SYN+ACK。

        通信双方收发数据前向数据包分配初始序号,并向对方通知此序号,这都是为了防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠的数据传输。

3、最后主机A向主机B传递如下消息:

[ACK]  SEQ: 1001,  ACK: 2001

因为主机A发送的 SYN 数据包需要消耗一个序号,因此此刻主机A发送的第二个数据包的序号在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递的信息含义是:“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包了。

        至此,主机A和主机B的TCP连接就建立成功了,接下来就可以进行数据传递操作了。

《TCP连接建立过程说明》

1、TCP传送数据的传输单位是TCP报文段,上面所说的数据包就是指的TCP报文段。

2、TCP报文段 = 首部 + 数据部分

3、TCP对TCP报文段分配序号,是以1个字节数据为单位分配的,而不是以TCP报文段个数为单位分配的。

4、在TCP三报文握手建立连接的过程中,SYN报文段需要消耗一个序号,即使该报文段的数据部分为空。

5、TCP报文段首部中的序号字段是该报文段的数据部分第1个数据字节的序号值。相关内容详情可以去了解TCP报文段的首部格式。

2.2 TCP内部工作原理2:与对方主机的数据交换

        通过第一步三报文握手过程成功建立起了TCP连接,完成了数据交换的准备工作,就下来就可以正式开始收发数据过程。

1、其默认方式如下图所示:

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第4张图片 图2-4  TCP套接字的数据交换过程

         上图2-4 给出了主机A分2次(分2个TCP报文段)向主机B传递200字节数据的过程。首先,主机A通过第一个报文段发送100个字节的数据,报文段的SEQ为1200。主机B为了确认收到该报文段,向主机B发送 ACK 1301 确认。

        此时的ACK号(确认号)为1301而非1201,原因在于ACK号的增量为传输的数据字节数。假设每次ACK号不加传输的字节数,这样虽然可以确认报文段的传输,但无法明确100字节数据是全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下公式传递ACK消息:

ACK号 = SEQ号 + 传递的数据字节数 + 1

        与三报文握手过程相同,最后加1是为了告知对方下次要传递的SEQ号。

2、传输数据过程中报文段丢失的情况,如下图所示:

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第5张图片 图2-5  TCP套接字数据传输中发生错误

         上图2-5 表示通过SEQ 1301 报文段向主机B传递100字节的数据。但中间发生了错误,主机B并未收到。经过一段时间后,主机A仍未收到对于 SEQ 1301 的ACK确认,因此主机A会重传该报文段。为了完成报文段的重传,TCP套接字会启动超时计时器以等待ACK应答。若超时计时器发生超时(Time-out)则重传。

2.3 TCP内部工作原理3:断开与套接字的连接

        TCP套接字断开连接的过程也非常优雅。如果对方还有数据需要传输时直接断开连接会出现问题,所以断开连接时需要双方进行友好协商。断开连接时双方对话如下:

  • 套接字A:“我希望断开连接。”
  • 套接字B:“哦,是吗?请稍后。”

  • 套接字B:“我也准备就绪,可以断开连接。”
  • 套接字A:“好的,谢谢合作。”

        先由套接字A向套接字B传递断开连接的消息,套接字B发出确认收到的消息,然后向套接字A传递可以断开连接的消息,套接字A同样发出确认消息,如下图所示:

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第6张图片 图2-6  TCP套接字断开连接过程

         上图2-6中,报文段内的 FIN 表示断开连接。也就是说,双方各发送1次 FIN 报文段后断开连接。此过程经历4个阶段,因此又称四报文握手(Four-way handshaking)。SEQ 和 ACK 的含义与前面讲解的含义一样。在上图2-6中,主机B向主机A传递了两次 ACK 5001,这是因为第二次FIN 报文段中的ACK 5001 只是因为接收ACK消息后未接收数据而重传给主机A的,以便其在要发出的第四个确认报文段中知晓自己的SEQ。

三 习题

1、请说明TCP套接字连接建立的三次握手过程。尤其是3次数据交换过程每次收发的数据内容。

第一次握手:客户端发送 SYN同步报文段,设置初始序号seq=x,并进入到 SYN-SENT状态,等待服务器端的确认。

第二次握手:服务器端收到客户端的SYN报文段后,回复 SYN+ACK 报文段,设置初始序号seq=y,确认号ack=x+1,并进入到 SYN-RCVD状态。

第三次握手:客户端收到服务器端发来的 SYN+ACK报文段后,回复 ACK 确认报文段,序号seq=x+1,确认号ack=y+1,并进入到 ESTABLISHED 状态,当服务器端收到 ACK报文段后,也将进入到 ESTABLISHED 状态。至此,TCP连接建立成功。

2、TCP是可靠的数据传输协议,但在通过网络通信的过程可能丢失数据。请通过ACK和SEQ说明TCP通过何种机制保证丢失数据的可靠传输。

:TCP通过在TCP报文段首部中设置SEQ(序号)和ACK(确认号)字段,就可以知道传输的数据是否正确地被通信对端接收。SEQ表示当前发送的TCP报文段的第一个数据字节的序号,ACK表示期望收到对方下一个报文段的第一个数据字节的序号。当收到某个确认报文段时,若确认号ACK=N,则表明到序号 N-1 为止的所有数据对方都已正确收到。若等待确认报文段超时,则说明传输的数据可能丢失,需要重传。

3、TCP 套接字中调用 write 和 read 函数时数据如何移动?结合 I/O 缓冲进行说明。

:一个TCP套接字是有独立地接收缓冲和发送缓存的,它们是操作系统内核区分配的内存空间。当TCP套接字调用write函数时,就是将待发送数据移至TCP的发送缓冲区中,而调用read函数时,就是将接收到的数据移至TCP的接收缓冲区。

4、对方主机的接收缓冲剩余 50 字节空间时,若本主机通过 write 函数请求传输 70 字节,请问 TCP 如何处理这种情况?

:根据TCP的流量控制机制,接收端主机会向发送端通知自己的接收窗口大小,作为让发送端设置其发送窗口大小的依据。因此,发送端的发送窗口只能先设置为50字节,然后发送出去50字节的数据,接收端收到这50字节的数据后,此时接收端的接收缓冲已满,接收端返回确认报文段,并通知发送端自己的接收窗口为零了,于是发送端就停止发送数据。当接收端的接收缓冲区中的数据被读取走后,并有足够的缓冲空间来接收剩余的数据时,接收端会通知发送方可以继续发送数据了,于是发送端就将剩余的20字节数据发送给接收端。

5、更改之前博文中编写的tcp_server.c 和 tcp_client.c程序,使服务器端和客户端各传递1次字符串。考虑到使用TCP传输协议,所以传递字符串前先以4字节整数型方式传递字符串长度。连接时服务器端和客户端数据传输格式如下。

Linux网络编程 - 基于TCP的服务器端/客户端(2)_第7张图片

 另外,不限制字符串传输顺序及种类,但须进行3次数据交换。

 相关博文链接:Linux网络编程 - 套接字与协议族  — 3.5 面向连接的套接字:TCP套接字示例

  • 服务器端程序 sendrecv_serv.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock;
    int clnt_sock;
    int str_len, i;

    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_sz;

    char msg1[]="Hello client!";
    char msg2[]="I'm server.";
    char msg3[]="Nice to meet you.";
    char *str_arr[]={msg1, msg2, msg3};    //指针数组指向三个字符串
    char read_buf[100] = {0};
    
    if(argc!=2){
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }
    
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("bind() error"); 
    
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
    
    clnt_addr_sz=sizeof(clnt_addr);  
    clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_sz);
    if(clnt_sock==-1)
        error_handling("accept() error");  
    
    for(i=0; i<3; i++)    //进行3次数据交互
    {
        str_len=strlen(str_arr[i])+1;              //字符串长度+1是包含了字符串结束符'\0'
        write(clnt_sock, (char*)(&str_len), 4);    //发送字符串长度的整型数信息
        write(clnt_sock, str_arr[i], str_len);     //发送字符串信息
        
        read(clnt_sock, (char*)(&str_len), 4);     //读取字符串长度整型数信息
        read(clnt_sock, read_buf, str_len);        //读取字符串信息,并存入字符数组中
        puts(read_buf);
    }
    
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 客户端程序 recvsend_clnt.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

void error_handling(char *message);

int main(int argc, char* argv[])
{
    int sock;
    struct sockaddr_in serv_addr;

    char msg1[]="Hello server!";
    char msg2[]="I'm client.";
    char msg3[]="Nice to meet you too!";
    char* str_arr[]={msg1, msg2, msg3};
    char read_buf[100];

    int str_len, i;
    
    if(argc!=3){
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }
    
    sock=socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));
    
    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
        error_handling("connect() error!");

    for(i=0; i<3; i++)    //进行3次数据交互
    {
        read(sock, (char*)(&str_len), 4);     //读取字符串长度整型数信息
        read(sock, read_buf, str_len);        //读取字符串信息,并存入字符数组中
        puts(read_buf);

        str_len=strlen(str_arr[i])+1;         //字符串长度+1是包含了字符串结束符'\0'
        write(sock, (char*)(&str_len), 4);    //发送字符串长度的整型数信息
        write(sock, str_arr[i], str_len);     //发送字符串信息
    }
    close(sock);
    return 0;
}

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

6、实现收发文件的服务器/客户端程序,实现顺序如下。

  • 客户端接收用户输入的传输文件名。
  • 客户端请求服务器传输该文件名所指的文件。
  • 如果该文件存在,服务器端就将其发送给客户端;反之,则回复文件不存在的提示信息。
  • 断开连接。
  • 文件服务器端程序 file_serv.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE  1024
#define FILE_SIZE 30

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    char buf[BUF_SIZE] = {0};
    char file_name[FILE_SIZE] = {0};
    FILE *fp;
    int read_cnt;
    
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t clnt_addr_sz;
    
    if(argc!=2){
        printf("Usage: %s \n", argv[0]);
        exit(1);
    }
    
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("bind() error"); 
    
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
    
    clnt_addr_sz=sizeof(clnt_addr);
    while(1)
    {
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
        if(clnt_sock==-1)
            error_handling("accept() error");
        read(clnt_sock, file_name, FILE_SIZE);           //接收文件名信息
        fp = fopen(file_name, "rb");                     //以二进制只读方式打开文件
        if(!fp){
            sprintf(buf, "%s file does`t exist!", file_name);
            write(clnt_sock, buf, BUF_SIZE);             //发送文件不存在提示信息
        }
        else{
            while(1)                                     //循环体中发送文件内容
            {
                read_cnt = fread(buf, 1, BUF_SIZE, fp);  //读取文件内容,按字节单位读取BUF_SIZE个元素
                if(read_cnt < BUF_SIZE)                  //文件内容读取完毕的条件
                {
                    write(clnt_sock, buf, read_cnt);
                    break;
                }
                write(clnt_sock, buf, BUF_SIZE);
            }
        }
        fclose(fp);
        close(clnt_sock);
        memset(buf, 0, BUF_SIZE);
    }
    
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 文件客户端程序 file_clnt.c
#include 
#include 
#include 
#include 
#include 
#include 

#define BUF_SIZE  1024
#define FILE_SIZE 30

void error_handling(char *message);

int main(int argc, char *argv[])
{
    int sock;
    FILE *fp;
    
    char buf[BUF_SIZE] = {0};
    char file_name[FILE_SIZE] = {0};
    int read_cnt;
    struct sockaddr_in serv_addr;
    
    if(argc!=3) {
        printf("Usage: %s  \n", argv[0]);
        exit(1);
    }
    
    printf("Input file name: ");
    scanf("%s", file_name);
    fp=fopen(file_name, "wb");                       //以二进制可写方式打开文件
    if(!fp)
        error_handling("fopen() error!");

    sock=socket(PF_INET, SOCK_STREAM, 0);   
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));

    if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error!")
    
    write(sock, file_name, strlen(file_name)+1);     //发送文件名字符串,+1表示包含了字符串结束符'\0' 

    while((read_cnt=read(sock, buf, BUF_SIZE))!=0)   //在while循环中将收到的read_cnt大小的文件数据块按字节单位写入文件
        fwrite((void*)buf, 1, read_cnt, fp);
    
    fclose(fp);
    close(sock);
    return 0;
}

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

参考

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

C语言fread和fwrite函数详解

你可能感兴趣的:(#,网络编程,Linux网络编程,socket编程,TCP/IP网络编程,TCP编程)