当前位置:   article > 正文

《TCP/IP网络编程》第4,5章知识点汇总_clnt_addr_size=sizeof(clnt_addr)

clnt_addr_size=sizeof(clnt_addr)

4. 基于TCP的服务器端/客户端(1)

4.1 TCP和UDP

TCP套接字是面向连接的,因此又称基于流(stream)的套接字。
TCP,Transmission Control Protocol,传输控制协议

四层网络模型

在这里插入图片描述

四层TCP/IP协议栈

在这里插入图片描述

链路层:最底层,定义LAN 、WAN 、MAN等网络标准
数据链路层通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路,为IP 层提供数据传送服务。

IP层:传输路径选择。IP本身是面向消息的、不可靠的协议。lP协议无法应对数据错误。
网络层通过路由选择算法,为分组选择最适当的路径,实现两个端系统之间的数据透明传送。

TCP/UDP:以IP层提供的路径信息完成实际的数据传输。IP只是负责传输数据包,不关心顺序、丢失或损坏的情况,TCP解决这些问题

应用层:在进行网络编程时,前3层的实现都被隐藏在socket之中,不需要我们多加操作。利用套接字编写程序,根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这便是应用层协议网络编程的大部分内容就是设计并实现应用层协议。

4.2 简单的TCP服务器端/客户端

以前面的 hello_server.c 服务器端和 hello_client.c 客户端为例,说明这个流程(注意顺序)

(1)服务器端:调用 socket() 和 bind() 创建套接字并为套接字分配地址信息

(2)服务器端:调用 listen() 进入等待连接请求状态

只有服务器端调用了listen函数,客户端才能进入可发出连接请求的状态(调用connect())。若客户端提前调用connect()将报错
listen()并不代表建立连接,它只是创建了一个连接请求队列,这个队列由第2个参数backlog决定大小。当客户端发起连接请求时,listen将其加入到连接请求队列之中,等待着连接受理。

(3)客户端:调用 connect() 发出连接请求(默认已用socket()创建套接字)

调用connect()之后,只有服务器端“接收连接请求”或者“发生断网等异常情况而中断连接请求”后才能返回值
这里的“接收连接”并不是指服务器端调用accept(),而是指请求被listen()记录到了等待队列之中
因此connect()返回后,并不能立即进行数据交换,因为此时连接尚未受理

(4)服务器端:调用 accept() 受理客户端连接请求

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen)
  • 1

sock选择监听套接字;addr保存客户端地址信息。
accept()受理sock创建的等待队列中的连接请求。它将自动创建一个新的套接字,并自动与该客户端建立连接,数据传输。
如果此时等待队列中为空,accept()将不会返回(阻塞),直到有连接请求到来为止

为什么要新建套接字:sock本身是一个监听套接字,如果将它用于和客户端的通信,那么它就无法正常接收新的连接请求了(难以区分哪些是请求连接的套接字信息,哪些是传输的数据),因此对于每一个客户端的连接请求,都要新建一个对应的套接字。

(5)服务器端/客户端:调用 read() write() 传输数据

accept()后,就有了一对一用于传输数据的套接字。此时,服务器端和客户端就可以向指定的socket进行读写了

(6)服务器端/客户端:close() 关闭套接字

思考:为什么客户端不用bind()为套接字分配地址信息?

客户端使用socket()之后立即使用connect(),似乎并未替socket分配地址信息。但实际上,网络数据交换的双方必须都分配IP地址和端口号。客户端的IP地址和端口号在调用connect()时由操作系统自动分配,它的IP地址即是自身计算机IP地址,端口号随机分配。

connect()的第1个参数是自己的sockfd,这个sockfd会被自动绑定客户端的IP地址和端口号;第2个参数就是服务器端的IP地址和端口号

(客户端连接服务器端时,需要服务器端的端口号。服务器端回复客户端时,也需要客户端的端口号。这两个端口号不一致)

4.3 迭代服务器端/客户端(echo)

服务器端和客户端不断互传信息,直到客户端输入Q为止

注意:此时服务器端在同一时刻只能处理一个客户端 —— 依次处理5个客户端的请求

服务器端 echo_server.c

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

#define BUF_SIZE 1024

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

int main(int argc, char* argv[]){

    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    int clnt_addr_size;
    int i, str_len;
    char message[BUF_SIZE];

    //1. 从这里开始和之前的 hello_server.c 一样
    if(argc != 2)
    {
        error_handling("wrong argc");
        exit(1);
    }
    //socket
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error!");
    //bind
    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!");
    //listen
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error!");
    
    //2. 使用循环,依次接受accept
    clnt_addr_size = sizeof(clnt_addr);
    for(i=0; i<5; ++i){
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        if(clnt_sock == -1)
            error_handling("accept() error!");
        else
            printf("Connected client %d \n", i+1);
        //反复接收数据,再返回给客户端;每次读取BUF_SIZE个字节
        while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0)
            write(clnt_sock, message, str_len);
        close(clnt_sock);
    }
    close(clnt_sock);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

客户端 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)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

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

    //1. 前面的部分和hello_client.c差不多
    if(argc != 3)
    {
        error_handling("wrong argc");
        exit(1);
    }

	//socket
	sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error!");
	//connect
	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 ...");
    	//若调用该函数引起的连接请求被注册到服务器端等待队列,则connect函数将完成正常调用。
    	//即使输出了“connected ...”,但如果服务器尚未调用accept函数,也不会真正建立服务关系。
	
    //2. 反复发送数据,再反复接收
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        //strcmp相等返回0
        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        write(sock, message, strlen(message));
        str_len = read(sock, message, BUF_SIZE-1);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
	close(sock);//调用close()向服务器发送EOF(意味着中断连接)
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

运行结果

在这里插入图片描述

echo服务器端/客户端存在的问题:
场景:服务器端从socket中读取数据,每次读多少就发送给客户端多少。设想当数据量很大的情况,此时服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整
解决:下一章详细说明

思考:关于accep()
首先,服务器端创建 serv_sock,这个serv_sock绑定了服务器端的ip地址和设置的端口号
然后,客户端创建 sock,这个sock绑定了自己的地址和端口,通过connect发送到服务器端
服务器端 accept 接收到请求,解析客户端 sock 中的地址和端口,并创建一个新的 clnt_sock

服务器端创建的serv_sock和clnt_sock,文件描述符fd分别是3,4
客户端创建的sock,文件描述符为3
服务器端和客户端创建的3个socket是不一样的

服务器端读写在clnt_sock,客户端读写在sock
问题:clnt_sock是怎么和sock传输数据的?(socket连接)

windows平台下的echo

(1)修改头文件,添加 WSAStartup() 语句
(2)修改变量类型名字,如socket的返回值类型要从 int 修改为 SOCKET
(3)bind()函数的返回值判断由 -1 修改为 SOCKET_ERROR
(4)read/write 修改为 send/recv (参数变为4个);close变为closesocket
都是一些小的修改,和前面linux下的代码差不多的。

5. 基于TCP的服务器端/客户端(2)

5.1 echo存在的问题和客户端修改

回顾前面的迭代服务器端/客户端的问题:无法达到TCP无数据边界的要求
如数据太大,服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整

服务器端收发数据的代码:

//反复读取,每次读多少就返回给客户端多少
while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0)
	write(clnt_sock, message, str_len);
  • 1
  • 2
  • 3

客户端收发数据的代码:

while(1)
{
    fputs("Input message(Q to quit): ", stdout);
    fgets(message, BUF_SIZE, stdin);
    if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
        break;
    //重点语句
    //write写数据没有问题,每次将字符串写完
    write(sock, message, strlen(message));
    //read的问题:假设服务器端发送的慢,一次read不能读完整怎么办?
    //服务器端没有这个问题是因为,服务器端用的是while+read,而不是一次性read
    //服务器端不断read/write,不关心每次传输的数据大小。客户端应当确定每次read多少,每次write多少
    str_len = read(sock, message, BUF_SIZE-1);
    
    message[str_len] = 0;
    printf("Message from server: %s", message);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

一种修改办法,是在客户端读取时,while循环+指定读取大小,如下

//控制客户端读取的字节数
str_len = write(sock, message, strlen(message));
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[recv_len] = 0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这种方式需要客户端自己清楚要接收的大小(这种很多情况做不到)。

5.2 应用层协议层面上进行修改

收发数据过程中定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。例如:“收到Q就终止连接”

在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。

案例:客户端向服务器端发送加减乘除的运算要求,服务器端计算结果并返回给客户端
在这里插入图片描述

自行尝试(跳过)

(之后再回来看存在哪些问题)

op_server.c

clnt_addr_size = sizeof(clnt_addr);
for(i=0; i<5; ++i)
{
    num_count = 0;
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    printf("client fd: %d\n", clnt_sock);
    while(1)
    {
        if((read_len=read(clnt_sock, buf, BUF_SIZE)) != 0)//0表示到达结尾
        {
            if(read_len == -1)
                error_handling("read() error");
            if(buf[0]<'0' || buf[0]>'9')
                break;
            nums[num_count++] = atoi(buf);
            printf("%d\n", nums[num_count-1]);
        }
        else
            break;
    }
    op = buf[0];
    printf("op is %c\n", op);
    res = 0;
    switch(op)
    {
        case '+':
            for(j=0; j<num_count; ++j)
                res+=nums[j];
            break;
        default:
            printf("no such operator!\n");
            break;
    }
    printf("return result: %d\n", res);
    putchar('\n');
    sprintf(buf, "%d", res);
    write(clnt_sock, buf, sizeof(buf));
    close(clnt_sock);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

op_client.c

fputs("Operand count: ", stdout);
scanf("%d", &num_count);
getchar();
for(i=0; i<=num_count; ++i){
    if(i<num_count)
        printf("Operand %d: ", i+1);
    else
        printf("Operator: ");
    fgets(message, BUF_SIZE, stdin);
    write(sock, message, strlen(message));
}
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s\n", message);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

书本代码(协议方式)

制定协议

定义协议如下:
(1)客户端连接到服务器端后以1字节整数形式传递待运算数字个数
(2)客户端向服务器端传递的每个整数型数据占用4字节
(3)传递整数型数据后接着传递运算符。运算符信息占用1字节
(4)选择字符+,-,*之一传递
(5)服务器端以4字节返回运算结果
(6)客户端得到运算结果之后终止与服务器端的连接

在这里插入图片描述

若想在数组中保存并传输多种类型,应当声明为 char 类型

!!从协议的角度解决问题时,尽可能从指针和字节的角度理解read和write,像 int 和 char 这样的类型理解为 4字节 和 1字节
协议就是规定双方的读写规则

op_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
#define OPSZ 4
#define RLT_SIZE 4

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

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
    int opnd_cnt, i;
    char opmsg[BUF_SIZE];
    int result;


    if(argc != 3)
    {
        error_handling("wrong argc");
        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);
    //协议1:用1个字节保存运算数的个数,最高为255个,即 0b11111111,更大的会被截断
    opmsg[0] = (char)opnd_cnt;
    for(i=0; i<opnd_cnt; ++i)
    {
        printf("Operand %d: ", i+1);
        //协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节
        scanf("%d", (int *)&opmsg[i*OPSZ+1]);//这行代码详解放在了下面
    }
    //协议3:用1个字节保存运算符+、-、*
    fgetc(stdin);//读取字符时要注意缓冲区残留的'\n'问题
    fputs("Operator: ", stdout);
    scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);
    //协议4:服务器端返回的结果用4个字节保存
    write(sock, opmsg, opnd_cnt*OPSZ+2);
    read(sock, &result, RLT_SIZE);//RLT_SIZE也是4字节,result是int类型
    
    printf("Message from server: %d\n", result);
    
	close(sock);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

关于scanf读取int型数据,但存放在 char str[] 数组的过程:

scanf("%d", (int *)&opmsg[i*OPSZ+1]);//每个运算数不管大小要占4个字节
//相当于以int读取一个数,例如384,它占4个字节,即32bit,二进制是 (0b)00000000 00000000 00000001 10000000
//将这4个字节放到 &opmsg[i*OPSZ+1] 这一地址指向的内存空间,因为opmsg是char类型数组,一个元素占1字节,因此要占4个元素
//假设下标从1开始,从数值的角度来看,则opmsg[1]=0b10000000=-128, opmsg[2]=0b00000001=1, opmsg[3]=0, opmsg[4]=0;
//即使不用(int *)显式说明也是可以的
//服务器端再将opmsg转为int型(例如一次性读4个字节),就能得到对应的int数值
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
op_server.c

和客户端采用同样的协议规则,重点在几条协议的实现代码上

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

#define BUF_SIZE 1024
#define OPSZ 4

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

int calculate(int opnum, int opnds[], char op)
{
    int result = opnds[0], i;
    switch(op)
    {
    case '+':
        for(i=1; i<opnum; ++i) result+=opnds[i];
        break;
    case '-':
        for(i=1; i<opnum; ++i) result-=opnds[i];
        break;
    case '*':
        for(i=1; i<opnum; ++i) result*=opnds[i];
        break;
    default:
        printf("no such operator!");
        break;
    }
    return result;
}

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    int i, j, clnt_addr_size;
    char opinfo[BUF_SIZE];
    int opnd_cnt, recv_len, recv_cnt;
    int result;

    if(argc != 2)
    {
        error_handling("wrong argc");
        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_size = sizeof(clnt_addr);
    for(i=0; i<5; ++i)
    {
        opnd_cnt = 0;
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        printf("connect socket: fd = %d\n", clnt_sock);
        //协议1:用1个字节保存运算数的个数。这里将1字节的内容转给4字节的int类型
        read(clnt_sock, &opnd_cnt, 1);
        //下面服务端将所有传输过来的内容放到opinfo里面
        recv_len = 0;
        //opnd_cnt*OPSZ+1 表示剩下还有多少字节没有读取
        while((opnd_cnt*OPSZ+1)>recv_len)
        {
            recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
            recv_len += recv_cnt;
        }
        //协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节
        //协议3:用1个字节保存运算符+、-、*
        //这里用int *强转为int arr[],arr的最后一个元素是没有意义的运算符
        result=calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len-1]);
        printf("return value = %d\n", result);
        //协议4:服务器端返回的结果用4个字节保存
        write(clnt_sock, (char *)&result, sizeof(result));
        close(clnt_sock);
    }
    close(serv_sock);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
运行结果

在这里插入图片描述

5.3 TCP套接字的I/O缓存

I/O缓冲

在这里插入图片描述

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

TCP使用滑动窗口,保证每次传输的数据不会超过接收方的输入缓冲的剩余空间大小
write/send 函数返回的时间节点:在数据移动到发送方的输出缓冲后即返回,TCP会保证输出缓冲中的数据传输给接收方

TCP内部工作原理1:三次握手

在这里插入图片描述

套接字是以全双工( Full-duplex )方式工作的。也就是说,它可以双向传递数据

以A向B发起连接为例

(1)第1次 A --> B

[SYN] SEQ: 1000, ACK: -
  • 1

SEQ sequence,表示序号;代表这是当前A传输给B的数据包,序号是1000
ACK 确认消息,表示希望从对方那里收到的下一个数据包序号。- 表示空,意味着这是首次连接

“现传递的数据包序号为1000 ,如果接收无误,请通知我向您传递1001号数据包。”

(2)第2次 B --> A

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

SEQ 代表这是B传给A的数据包,序号是2000
ACK 确认消息,表示之前已经收到过A传给B的1000,希望下次收到的是1001

“现传递的数据包序号为2000 ,如果接收无误,请通知我向您传递2001 号数据包。“

(3)第3次 A --> B

[ACK] SEQ: 1001, ACK 2001
  • 1

含义与第2次类似。TCP保证了有序传输
到这里,A和B各发送了一次确认消息ACK,确认了彼此均就绪

TCP内部工作原理2:数据交换

在这里插入图片描述

每次收到的确认号 ACK = SEQ + 传输的字节数 + 1
加上传输的字节数,是为了检查是否所有数据都被收到了
最后加1,是为了告知对方下次希望收到的 SEQ

TCP处理传输错误的情况:

在这里插入图片描述

接收方每收到一个数据包,都会发送ACK确认,如果数据包丢失,就不会发送ACK。
发送方发送一个数据包后,超过一定时间还没有收到ACK,就认为数据包丢失,于是重传数据包
(即便发送方超时重传后又收到了第一次发送的ACK,也没有关系。因为接收方收到重复的数据包会舍弃,被舍弃的数据包不会为它发送ACK)

快速重传:
A发送 S1, S2, S3, S4 共4个数据包给B,S1顺利到达,于是B返回确认号 A2;
S2延迟或丢失,于是S3和S4先一步到达。此时发现还是没有S2,于是发出的确认号都是 A2。
A收到3个一样的确认号 A2,就知道S2缺失,于是发送S2

TCP内部工作原理3:断开连接

在这里插入图片描述

FIN表示断开连接,双方各发送一次FIN,并且各收到一次ACK后,连接断开,又称为四次握手

注意的是,中间主机B连续两次发送数据包
主机B的FIN中,再一次使用 ACK 5001 是表示上一次只是为了接收ACK,并没有接收A发来的数据,所以要求A重传一次

5.4 练习

收发文件的服务器端和客户端

  • 客户端接受用户输入的传输文件名。
  • 客户端请求服务器端传输该文件名所指文件。
  • 如果指定文件存在,服务器端就将其发送给客户端;反之,则断开连接。

存在的问题:当服务器端和客户端之间有数据传输时,服务器端调用close(),客户端接收到的是-1,而不是0
原因:只要TCP栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端发送RST,而不是FIN
解决方法:在close()之前,使用shutdown(clnt_sock, SHUT_WR)。详见第7章

file_server.c

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

#define BUF_SIZE 1024

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

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    int i, clnt_addr_size, file_fd, read_len;
    char buf[BUF_SIZE];
    char file_name[BUF_SIZE];

    if(argc != 2)
    {
        error_handling("wrong argc");
        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_size = sizeof(clnt_addr);
    for(i=0; i<5; ++i)
    {
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        printf("connected client fd = %d\n", clnt_sock);
        if(clnt_sock == -1)
            error_handling("accept error");
        read(clnt_sock, file_name, BUF_SIZE-1);
        printf("file name = %s\n", file_name);
        file_fd = open(file_name, O_RDONLY);
        if(file_fd == -1)
        {
            close(clnt_sock);
            fputs("can not find the file!", stdout);
            break;
        }
        while((read_len = read(file_fd, buf, BUF_SIZE-1)) > 0)
        {
            printf("read size: %d\n", read_len);
            write(clnt_sock, buf, read_len);
        }
        close(file_fd);
        close(clnt_sock);
    }
    close(serv_sock);

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

file_client.c

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

#define BUF_SIZE 1024
#define NAME 64

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

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
    int recv_len, i, file_fd;
    char buf[BUF_SIZE];
    char file_copy[BUF_SIZE];


    if(argc != 3)
    {
        error_handling("wrong argc");
        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("enter the file name: ", stdout);
    scanf("%s", file_copy);
    write(sock, file_copy, sizeof(file_copy));

    strcat(file_copy, "_copy.c");
    while((recv_len = read(sock, buf, BUF_SIZE-1)) != 0)
    {
        if(recv_len == -1)
        {
            error_handling("read() error!");
        }
        //注意,如果用open是新建一个文件,最好加上八进制的权限设置,如0644(权限设置和linux文件操作一样)
        file_fd = open(file_copy, O_CREAT|O_APPEND|O_WRONLY, 0644);
        write(file_fd, buf, recv_len);
        close(file_fd);
    }
    
    close(sock);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/158748
推荐阅读
相关标签
  

闽ICP备14008679号