当前位置:   article > 正文

【Linux开发—网络编程】_如何在linux下进行网络编程

如何在linux下进行网络编程

  • 网络编程,编写程序使两天或多台联网的计算机之间交互数据。

一,网络—介绍

ctontrol + C:强制结束进程

1,网络—物理结构

1,普通网络

在这里插入图片描述

2,光纤千兆网络

  • 市面上很多千兆网,但普通的网线最大吞吐量不过百兆,想达到千兆需要换千兆光纤网线,还需要千兆光纤转接口
  • 千兆网线线型扁平,接口类似USB那种小状
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2,网络—IP地址

并不是所有的地址都是可以用的

  • 1,网络地址是由4字节32位构成,

  • 2,以0开头的地址,都是不可以用的

  • 3,以0结尾的地址,表示网段,而不是具体地址

  • 4,224到239开头的地址,是组播地址,不可用于点对点的传输

    • 组播:可理解位TCP/UDP上面的广播
    • 优点:组播可以极大节约带宽
    • 弊端:容易形成网络风暴
  • 5,240到255开头的地址,实验用的,保留,一般不做服务器或者终端地址

    • 127.0.0.1 保留的,回环网络地址,往这个网络发任何数据,都会被回发回来,一般用于学习和测试使用。
    • 0.0.0.0 保留的,全网段地址,一般用于服务器监听
  • A(大型网络)、B(中型网络)、C(小型网络)类用于不同规模的网络

  • D 类用于组播(类似广播)

  • E 类保留,用于实验、私有网络
    在这里插入图片描述

3,网络—端口

端口并非只有服务器才会使用,客户端也一样会使用。

  • 公认端口(Well Known Ports):又称常用端口,从0到1024,紧密绑定于一些特定服务。通常这些端口的通信明确表明了某种服务的协议,这种端口是不可再重新定义它的作用对象。netstat -an
    在这里插入图片描述

    • 443: https服务
    • 80:http通信
    • 23:Telnet服务
  • 注册端口(Registered Ports):端口号从1025到49151,这些端口多数没有明确定义服务对象,不同程序可根据实际需求自定义,远程控制软件和木马程序都会由这些端口的定义。

  • 动态和/或私有端口(Dynamic and/or Private Ports):端口号从49152到65535(不要轻易作为服务器的监听端口)。特别是一些木马程序非常喜欢用这些端口,这些端口常常不被注意,易隐蔽。

4,网络—协议

协议就是,一种网络交互中数据格式和交互流程的约定。通过协议,我们可以与远程设备进行数据交互,请求服务或完成对方的服务。

1, TCP协议包头结构:

48字节
在这里插入图片描述

2,HTTP协议的包头结构:

文本流
GET请求,1.1版本,Host:向网站发送数据;UA:客户端浏览器信息;
Accept:接收信息,格式,语言类型
在这里插入图片描述

3,SSH数据包头结构

6字节,字节流
在这里插入图片描述

5,TCP协议

传输控制协议(TCP,Transmission Control Protocol),面向连接,可靠,基于字节流(以字节为单位) 的传输层通信协议。安全可靠但牺牲了性能。

  • 交互流程:
    在这里插入图片描述

    • 超时才能解决物理连接中断,默认的超时可能长达两个小时。
    • 应对超时方案:心跳包机制。强制询问双方是否在线,异常连续超过一定次数,一般3次心跳收不到或发不出去,就强制断开连接。

二,套接字—介绍

  • 同Windows网络编程中的网络部分类似Windows网络编程
  • 套接字(socket):网络数据传输用的软件设备,操作系统给我们提供套接字支持。

1,socket认识

在这里插入图片描述

2,socket创建

套接字有很多种,用的最多的TCP和UDP的套接字。

#include <sys/socket.h>

/* socket创建
- domain:使用的协议族(Protocol Family)信息
- type:数据传输类型信息
- protocol:计算机之间通信种使用的协议信息
*/
int socket(int domain, int type, int protocol);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3,socket函数

  • 面向连接的套接字TCP:是可靠的、按序传递的、基于字节的 面向连接的数据传输方式。
  • 面向消息的套接字UDP:不可靠的、不按序传递的、以数据的高速传输为目的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

4,bind函数

#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
/* socket绑定
- sockfd:socket套接字文件描述符
- myaddr:结构体,存放地址信息的结构体变量地址值,IPv4,IPV6
- addrlen:
*/
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);//绑定成功返回0,失败返回-1
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ntohs 可以理解为:把short型数据从网络字节序转化为主机字节序。

struct sockaddr_in addr;
char *serv_ip = "221.214.168.13";//声明IP地址字符串
char *serv_port = "9190";//声明端口号
memset(&add, e,sizeof(addr));//结构体变量addr的所有成员初始化为0

addr.sin_fimily = AF_INET;//指定地址族
/*
每次创建服务器端套接字都要输入IP地址,会很繁琐,此时可初始化地址信息为INADDRANY。
addr.sin_addr.s_addr = htonl(INADDRANY);
*/
add.sin_addr.s_addr = inet_addr(serv_ip);//基于字符串的IP地址初始化
add.sin_port=htons(atoi(serv_port));//基于字符串的端口号初始化

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

5,listen函数

#include <sys/socket.h>

/*
- sock:套接字文件描述符
- backlog:连接请求等待队列的长度,若为5,则队列长度为5,最多使5个连接请求进入队列
*/
int listen(int sock, int backlog);//成功返回0,失败返回-1.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4,accept函数

#include <sys/socket.h>

/*
- sock:套接字文件描述符
- addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后传递来的地址变量参数天才客户端地址信息
- addrlen: addr结构体参数的长度。
*/
int accept(int sock, struct sockaddr*addr, socklen_t *addrlen);//成功返回0,失败返回-1.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

三,TCP编程—回声服务器

  • 回声服务器:将从客户端收到的数据原样返回给客户端,即“回声”

1,TCP/IP 协议栈

基于TCP/IP网络协议的套接字分为:TCP和UDP套接字。

2,TCP服务端:

void server_func()
{
    printf("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);

    int server_sock, client_sock;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_addr_size;

    //此处省略了一些,socket的配置认证,eg:版本号等
    server_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        //error_handling("create socket error!");
        std::cout << "create socket error!\n";
        return;
    }

    memset(&server_addr, 0 , sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");// htonl(INADDR_ANY);  0.0.0.0 表示监听全网段,包括内外网
    server_addr.sin_port = htons(9444);

    int ret = bind(server_sock, (struct  sockaddr*)&server_addr, sizeof(server_addr));

    if (ret == -1) {
        //error_handling("bind socket error!");
        std::cout << "server bind socket error!\n";

        close(server_sock);
        return;
    };
    client_addr_size = sizeof(client_addr);
    ret = listen(server_sock, 3);
    if (ret == -1) {
        //error_handling("listen socket error!");
        std::cout << "server listen socket error!\n";

        close(server_sock);
        return;
    };

    //回声服务器-原理:服务端循环接收客户端的消息
    char buffer[1024];
    while (1)
    {
        memset(buffer, 0, sizeof(buffer));

        client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_size);
        if (client_sock == -1) {
            //error_handling("accept server socket error!");
            std::cout << "server accept socket error!\n";

            close(server_sock);
            return;
        }

        ssize_t len = 0;
        while ((len = read(client_sock, buffer, sizeof(buffer))) > 0)
        {
            len = write(client_sock, buffer, strlen(buffer));
            if (len != (ssize_t)strlen(buffer)) {
                //error_handling("write message failed!");
                std::cout << "server write message failed!\n";

                close(server_sock);
                return;
            }

            std::cout << "server read & write success!, buffer:" << buffer <<"__len:"<< len << std::endl;

            memset(buffer, 0, len);//清理
        }

        close(client_sock);//服务端关闭的时候,客户端会自动关闭
    };

  
    close(server_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
  • 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

3,TCP客户端:

void client_func()
{
    int client = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servaddr.sin_port = htons(9444);
    int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret == -1) {
        std::cout << "client connect failed!\n";
        close(client);
        return;
    }
    std::cout << "client connect server is success!\n";

    char buffer[256] = "";
    while (1)
    {
        fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
        fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
        if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
            break;
        }

        size_t len = strlen(buffer);
        size_t send_len = 0;

        //当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
        while (send_len < len)
        {
            ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
            if (ret <= 0) {//连接出了问题
                fputs("may be connect newwork failed,make client write failed!\n", stdout);
                close(client);
                return;
            }
            send_len += (size_t)ret;

            std::cout << "client write success, msg:" << buffer << std::endl;

        }
        memset(buffer, 0, sizeof(buffer));

        //当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
        size_t read_len = 0;
        while (read_len < len)
        {
            size_t ret = read(client, buffer + read_len, len - read_len);
            if (ret <= 0) {//连接出了问题
                fputs("may be connect newwork failed, make client read failed!\n", stdout);
                close(client);
                return;
            }
            read_len += (size_t)ret;
        }
        std::cout << "from server:" << buffer << std::endl;
    };
    
    close(client);
    std::cout << "client done!" << std::endl;
}
  • 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

4, 服务端与客户端不同进程交互

#include <sys/wait.h>
void server_client_connect()
{
    //创建进程
    pid_t pid = fork();
    if (pid == 0) {//为0时表示在子进程
        sleep(1);//为了让服务端先执行

        client_func();
        
    }
    else if (pid > 0) {//主进程为服务端进程
        server_func();

        int status = 0;
        wait(&status);
    }
    else
    {
        std::cout << "fork failed!" << pid << std::endl;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

5,回声服务器实战项目(网络计算器)—云计算

  • 需要源码的,还请留言。
  • 项目需求
/*
* 云计算器-需求:
1.客户端连接到服务器端后以1字节整数形式传递待算数字个数。      (个数是1字节)
2.客户端向服务器端传递的每个整数型数据占用4字节。             (每个数是4字节)
3.传递整数型数据后接着传递运算符。运算符信息占用1字节。       (运算符信息1字节)
4.选择字符+、-、*之一传递。
5. 服务器端以4字节整数型向客户端传回运算结果。
6. 客户端得到运算结果后终止与服务器端的连接。
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 最终输出:
    在这里插入图片描述

四,TCP底层原理

1,TCP套接字的 I/O 缓冲

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

  • 实际上,write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据。更准确地说,如下图所示,write函数调用瞬间,数据将移至输出缓冲;read函数调用瞬间,从输人缓冲读取数据。
    在这里插入图片描述

    • 1,发送数据到接收,中间有物理线缆和输入输出缓冲区影响,因此会慢一些
    • 2,server发送数据会先回访client有没有缓冲区(3次握手协议),有的话可以继续发送。
  • 调用write函数时,数据将移到输出缓冲,在适当的时候(不管是分别传送还是一次性传送)传向对方的输入缓冲。这时对方将调用read函数从输入缓冲读取数据。这些I/O 缓冲特性可整理如下。
    A: I/O缓冲在每个TCP套接字中单独存在。
    B: I/O缓冲在创建套接字时自动生成。
    C: 即使关闭套接字也会继续传递输出缓冲中遗留的数据
    D: 关闭套接字将丢失输入缓冲中的数据
    那么,下面这种情况会引发什么事情?理解了I/O缓冲后,其流程∶
    “客户端输入缓冲为50字节,而服务器端传输了100字节。”
    这的确是个问题。输入缓冲只有50字节,却收到了100字节的数据。可以提出如下解决方案∶
    填满输入缓冲前迅速调用read函数读取数据,这样会腾出一部分空间,问题就解决了

  • 其实根本不会发生这类问题,因为TCP会控制数据流。
    TCP中有滑动窗口(Sliding Window)协议,用对话方式呈现如下。
    套接字A∶"你好,最多可以向我传递50字节。"
    套接字B∶"OK!"
    套接字A∶"我腾出了20字节的空间,最多可以收70字节。
    套接字B∶"OK!"
    数据收发也是如此,因此TCP中不会因为缓冲溢出而丢失数据。
    但是会因为缓冲而影响传输效率

2,TCP的内部原理

  • TCP通信三大步骤:
    • 1, 三次握手建立连接;
    • 2, 开始通信,进行数据交换;
    • 3, 四次挥手断开连接;

1,三次握手

(客户端主动发起 - 服务端应答)

  • 【第一次握手】套接字A∶"你好,套接字B。我这儿有数据要传给你,建立连接吧。"
    【第二次握手】套接字B∶"好的,我这边已就绪。"
    【第三次握手】套接字A∶"谢谢你受理我的请求。"

在这里插入图片描述
首先,请求连接的主机A向主机B传递如下信息∶
[SYN] SEQ:1000, ACK: -
该消息中SEQ为1000,ACK为空,而SEQ为1000的含义如下∶
"现传递的数据包序号为1000,如果接收无误,请通知我向您传递1001号数据包。"这是首次请求连接时使用的消息,又称SYN。SYN是Synchronization的简写,表示收发数据前传输的同步消息

接下来主机B向A传递如下消息∶
[SYN+ACK]SEQ:2000, ACK:1001
此时SEQ为2000,ACK为1001,而SEQ为2000的含义如下∶
现传递的数据包序号为2000如果接收无误,请通知我向您传递2001号数据包。”

而ACK1001的含义如下∶
刚才传输的SEQ为1000的数据包接收无误,现在请传递SEQ为1001的数据包。”

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

收发数据前向数据包分配序号,并向对方通报此序号,这都是为防止数据丢失所做的准备。通过向数据包分配序号并确认,可以在数据丢失时马上查看并重传丢失的数据包。因此,TCP可以保证可靠的数据传输。最后观察主机A向主机B传输的消息∶
[ACK]SEQ:1001, ACK:2001

TCP连接过程中发送数据包时需分配序号。
在之前的序号1000的基础上加1,也就是分配1001。此时该数据包传递如下消息∶
“已正确收到传输的SEQ为2000的数据包,现在可以传输SEQ为2001的数据包。”

这样就传输了添加ACK2001的ACK消息。至此,主机A和主机B确认了彼此均就绪。

  • 特么的,文字太复杂了,给大家来个通俗易懂的。
    TCP 三次握手好比在一个夜高风黑的夜晚,你一个人在小区里散步,不远处看见小区里的一位漂亮妹子迎面而来,但是因为路灯有点暗等原因不能100%确认,所以要通过招手的方式来确定对方是否认识自己。
    你首先向妹子招手(syn),妹子看到你向自己招手后,向你点了点头挤出了一个微笑(ack)。你看到妹子微笑后确认了妹子成功辨认出了自己(进入established状态)。
    但是妹子有点不好意思,向四周看了一看,有没有可能你是在看别人呢,她也需要确认一下。妹子也向你招了招手(syn),你看到妹子向自己招手后知道对方是在寻求自己的确认,于是也点了点头挤出了微笑(ack),妹子看到对方的微笑后确认了你就是在向自己打招呼(进入established状态)。

利用TCP三次握手的攻击
客户端不断的只做第一次握手,这样服务端一直在根据这次握手不断的创建连接分配序号,但这些创建的连接需要两个小时左右的超时时间,,这样就比较占用服务端资源,数据量过大阻塞服务端。

2,数据传输

  • 数据传输中要注意:重传和去重,这两项工作操作系统的网络内核模块都已经帮我们处理好了。
    在这里插入图片描述
  • TCP 数据传输就是两个人隔空交流,有一定的距离,需要对方反复确认听见了自己的话。
    你喊了一句话(seq),妹子听见了之后要向你回复自己听见了(ack)。
    如果你喊了一句,半天没听到妹子回复,你会很低落,好比谈恋爱的时候,你满腔热情,而妹子忽冷忽热,所以你锲而不舍,一次不行,就两次,两次不行就三次,这就是tcp重传。既然会重传,妹子就有可能同一句话听见了两次,这就是去重。
    对于重传和去重这两项工作操作系统的网络内核模块都已经帮我们处理好了。
    完了之后,妹纸和你要依依不舍地分开了。
    那么就要分手了。
    也就是来到了TCP通信的四次挥手了。

3,四次挥手

在这里插入图片描述

五,UDP编程

UDP是DDOS攻击(网络分布式拒绝服务攻击)的最主要形式,可以伪造发件人地址,数据无法拒收,需要数据包接收,但由于伪造了发件人地址,拒收又找不到发件人

1,UDP基本原理

我们可以通过信件说明UDP的工作原理,这是讲解UDP时使用的传统示例,它与UDP特性完全相符。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认对方是否收到。另外,邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式。与之类似,UDP提供的同样是不可靠的数据传输服务。
“既然如此,TCP应该是更优质的协议吧?”

如果只考虑可靠性,TCP的确比UDP好。但UDP在结构上比TCP更简洁。UDP不会发送类似ACK的应答消息,也不会像SEQ那样给数据包分配序号。因此,UDP的性能有时比TCP高出很多。编程中实现UDP也比TCP简单。另外,UDP的可靠性虽比不上TCP,但也不会像想象中那么频繁地发生数据损毁。因此,在更重视性能而非可靠性的情况下,UDP是一种很好的选择。

既然如此,UDP的作用到底是什么呢?为了提供可靠的数据传输服务,TCP在不可靠的IP层进行流控制,而UDP就缺少这种流控制机制。
流控制是区分UDP和TCP的最重要的标志。但若从TCP中除去流控制,所剩内容也屈指可数。也就是说,TCP的生命在于流控制

如果把TCP比喻为电话,把 UDP比喻为信件。但这只是形容协议工作方式,并没有包含数据交换速率。请不要误认为"电话的速度比信件快,因此TCP 的数据收发速率也比 UDP快"。实际上正好相反。TCP的速度无法超过UDP,但在收发某些类型的数据时有可能接近 UDP。例如,每次交换的数据量越大,TCP的传输速率就越接近 UDP的传输速率。

TCP与UDP结合使用:用TCP做流控制,用UDP做数据传输。

UDP包数据传输的无序性,由于路由器的状态变化,可能先发的包后到

在这里插入图片描述

  • 从上图可以看出,IP的作用就是让离开主机B的UDP数据包准确传递到主机A。但把UDP 包最终交给主机A的某一UDP套接字的过程则是由UDP完成的。UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字
    其实在实际的应用场景中,UDP也具有一定的可靠性。网络传输特性导致信息丢失频发,可若要传递压缩文件(发送1万个数据包时,只要丢失1个就会产生问题),则必须使用TCP,因为压缩文件只要丢失一部分就很难解压。但通过网络实时传输视频或音频时的情况有所不同。对于多媒体数据而言,丢失一部分也没有太大问题,这只会引起短暂的画面抖动,或出现细微的杂音。但因为需要提供实时服务,速度就成为非常重要的因素,此时需要考虑使用UDP。但UDP并非每次都快于TCP,TCP比UDP慢的原因通常有以下两点。
    • 1 收发数据前后进行的连接设置及清除过程。
    • 2 收发数据过程中为保证可靠性而添加的流控制,
      尤其是收发的数据量小但需要频繁连接时,UDP比TCP更高效。

2,UDP套接字

  • UDP与TCP区别:

    • 1,UDP没有listen 和 accept
    • 2,UDP收发数据都在一个套接字上,根据from和recvfrom 发件人地址和收件人地址区分。
  • UDP 中的服务器端和客户端没有连接
    UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字的过程和数据交换过程。

  • UDP服务器端和客户端均只需 1个套接字
    TCP中,套接字之间应该是一对一的关系。若要向10个客户端提供服务,则除了守门的服务器套接字外,还需要10个服务器端套接字。但在UDP中,不管是服务器端还是客户端都只需要1 个套接字。之前解释UDP原理时举了信件的例子,收发信件时使用的邮筒可以比喻为UDP套接字。只要附近有1个邮筒,就可以通过它向任意地址寄出信件。同样,只需1个UDP套接字就可以向任意主机传输数据

在这里插入图片描述

  • 上图展示了1个UDP套接字与2个不同主机交换数据的过程。也就是说,只需1个UDP套接字就能和多台主机通信
  • 创建好TCP套接字后,传输数据时无需再添加地址信息。因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态(UDP套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前在信件中填写地址。以下为:填写地址并传输数据时调用的UDP相关函数。

1, sendto()函数—发件人

#include<sys/socket.h>
→成功时返回传输的字节数,失败时返回-1。
ssize_t sendto(int sock,void*buff,size_t nbytes,int flags,struct sockaddr *to, socklen_t addrlen);
  • 1
  • 2
  • 3

● sock
用于传输数据的UDP套接字文件描述符。
● buff
保存待传输数据的缓冲地址值。
● nbytes
待传输的数据长度,以字节为单位。
● flags
可选项参数,若没有则传递0。
● to
存有目标地址信息的sockaddr结构体变量的地址值。
● addrlen
传递给参数to的地址值结构体变量长度。

sendto 函数与之前的TCP输出函数最大的区别在于,此函数需要向它传递目标地址信息。接下来介绍接收UDP数据的函数。UDP数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回UDP数据包中的发送端信息

2,recvfrom()函数—收件人

#include<sys/socket.h>
//→成功时返回接收的字节数,失败时返回-1。
ssize_t recvfrom(int sock,  void *buff,size_t nbytes,  int flags,
struct sockaddr*from,   socklen_t*addrlen);
  • 1
  • 2
  • 3
  • 4

●sock用于接收数据的UDP套接字文件描述符。
●buff保存接收数据的缓存地址值
●nbytes 可接收的最大字节数,故无法超过参数buf所指的缓冲大小。
●flags可选项参数,若没有则传入0。
●from存有发送端地址信息的sockaddr结构体变量的地址值。
●addrlen 保存参数from的结构体变量长度的变量地址值。
编写UDP程序时最核心的部分就在于上述两个函数,这也说明二者在UDP数据传输中的地位。

3,UDP编程—回声服务器

1,服务端

  • 1,创建socket
  • 2,配置地址族(ip, port)
  • 3,绑定bind
  • 4,接收数据recvfrom,发送数据sendto
  • 5,关闭socket

2,客户端

  • 1,创建socket
  • 2,配置地址族(ip, port)
  • 3,发送数据sendto,接收数据recvfrom
  • 4,关闭socket

3,核心源码

//UDP回声服务器
#define BUF_SIZE 30 //buffer字节大小

//需要控制台 输入端口
void udp_server(int argc, char* argv[])
{
    if (argc != 2) {//校验端口参数,
        printf("Usage : %s <port>\n", argv[0]);
        error_handling("argement is error: 端口");
    }

    int serv_sock;
    char message[BUF_SIZE];
    socklen_t clnt_adr_sz;
    struct sockaddr_in serv_adr, clnt_adr;
    
    //创建socket
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);//UDP的socket SOCK_DGRAM: 报文类型
    if (serv_sock == -1)
        error_handling("UDP socket creation error");

    memset(&serv_adr, 0, sizeof(serv_adr));
    //配置socket的地址族
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY->0.0.0.0 htonl:主机字节序 转网络字节序 l:long型 4或8字节
    serv_adr.sin_port = htons((uint16_t)atoi(argv[1]));//需要输入端口 htons:主机字节序 转网络字节序 s:short型,2字节

    //绑定
    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
        
    //收发数据
    ssize_t str_len;
    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);//利用分配的地址接收数据。不限制数据传输对象
        sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);//函数调用同时获取数据传输端的地址。正是利用该地址将接收的数据逆向重传。
    }
    close(serv_sock);
}

void udp_client(int argc, char* argv[])
{
    if (argc != 3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        error_handling("argement is error: IP");
    }

    int sock;
    char message[BUF_SIZE];
    socklen_t adr_sz;

    struct sockaddr_in serv_adr, from_adr;
    
    //创建socket
    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == -1)
        error_handling("socket() error");

    memset(&serv_adr, 0, sizeof(serv_adr));//清理,防止默认值

    //配置socket的地址族
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);//控制台输入ip地址,eg:127.0.0.1
    serv_adr.sin_port = htons((uint16_t)atoi(argv[2]));//控制台输入端口

    //收发数据
    ssize_t str_len;
    while (1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))//字符串比较
            break;

        ssize_t len = sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
        memset(message, 0, len);
        adr_sz = sizeof(from_adr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);

        printf("Message from server: %s", message);
        message[str_len] = 0;
    }
    close(sock);
}


//主进程开启客户端,再开启服务端,是为了在主进程调试方便
void udp_server_client_connect(int argc, char* argv[])
{

    char* argv0 = "";
    pid_t pid = fork();
    if (pid < 0)
        std::cout << "fork failed!" << pid << std::endl;

    if (pid == 0)//开启新进程,子进程(服务端进程)
    {

        int argc = 2;
        char* argv[] = { argv0, (char*)"9555" };
        udp_server(argc, argv);
        
    }
    else//pid > 0 , 父进程(客户端进程) 
    {
        int argc = 3;
        char* argv[] = { argv0, (char*)"127.0.0.1", (char*)"9555" };
        udp_client(argc, argv);

        int status = 0;
        wait(&status);
    }

}
  • 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
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

3,UDP的传输特性和调用

  • 前面讲解了UDP服务器端/客户端的实现方法。但如果仔细观察UDP客户端会发现,它缺少把IP和端口分配给套接字的过程。
  • TCP客户端调用connect函数自动完成此过程,而UDP中连能承担相同功能的函数调用语句都没有。究竟在何时分配IP和端口号呢?
  • UDP程序中,调用sendto函数传输数据前应完成对套接字的地址分配工作,因此调用bind函数。
  • 当然,bind函数在TCP程序中出现过,但bind函数不区分TCP和UDP,也就是说,在UDP程序中同样可以调用。
  • 另外,如果调用sendto函数时发现尚未分配地址信息,则在首次调用sendto 函数时给相应套接字自动分配IP和端口。
  • 而且此时分配的地址一直保留到程序结束(关闭套接字之前)为止,因此也可用来与其他UDP套接字进行数据交换。
  • 当然,IP用主机IP,端口号选尚未使用的任意端口号。

综上所述,调用sendto函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的地址分配过程

六,套接字的多种可选项

1,I/O缓冲大小

1,认识:

在这里插入图片描述

2,getsockopt & setsockopt 函数

我们几乎可以针对上表中的所有可选项进行读取(Get)和设置(Set)(当然,有些可选项只能进行一种操作)。可选项的读取和设置通过如下2个函数完成。

#include<sys/socket.h>
//→成功时返回0,失败时返回-1。
int getsockopt(int sock, int level,int optname, void *optval, socklen_t *optlen);
  • 1
  • 2
  • 3

●sock :用于查看选项套接字文件描述符。
●level要查看的可选项的协议层。
●optname 要查看的可选项名。
●optval保存查看结果的缓冲地址值。
●optlen向第四个参数optval传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。

#include<sys/socket.h>
//→成功时返回0,失败时返回-1。
int setsockopt(int sock, int level, int optname, const void*optval, socklen_t optlen);
  • 1
  • 2
  • 3

●sock用于更改可选项的套接字文件描述符。
●level要更改的可选项协议层。
●optname 要更改的可选项名。
●optval 保存要更改的选项信息的缓冲地址值。
●optlen 向第四个参数optval传递的可选项信息的字节数。

3,SO_SNDBUF &SO_RCVBUF

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	int snd_buf=1024*3, rcv_buf=1024*3;
	int state;
	socklen_t len;
	
	sock=socket(PF_INET, SOCK_STREAM, 0);
	state=setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	state=setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
	if(state)
		error_handling("setsockopt() error!");
	
	len=sizeof(snd_buf);
	state=getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
	if(state)
		error_handling("getsockopt() error!");
	
	len=sizeof(rcv_buf);
	state=getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
	if(state)
		error_handling("getsockopt() error!");
	
	printf("Input buffer size: %d \n", rcv_buf);
	printf("Output buffer size: %d \n", snd_buf);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
  • 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

2,SO_REUSEADDR

  • 发生地址分配错误(Binding Error)地址重新分配

服务器由于迟迟得不到响应,出现Time-wait等待状态,

  • Time-wait解决方案就是:在套接字的可选项中更改SO_REUSEADDR的状态。适当调整该参数,可将Time-wait状态下的套接字端口号重新分配给新的套接字。SO_REUSEADDR的默认值为0(假),这就意味着无法分配Time-wait状态下的套接字端口号。因此需要将这个值改成1(真)。
	//解决Time-wait的问题
    getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
    printf("SO_REUSEADDR = %d\n", optval);
    //设置optval
    if (optval == 0)
        optval = 1;

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, addrlen);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • demo项目:
//------------------------ Time-wait--------------------------
#define TRUE 1
#define FALSE 0

void tw_tcp_server()
{
    int sock, client, optval = 0;
    struct sockaddr_in addr, cliAddr;
    socklen_t addrlen = sizeof(addr);
    char buffer[256] = "";

    sock = socket(PF_INET, SOCK_STREAM, 0);

    //解决Time-wait的问题
    getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
    printf("SO_REUSEADDR = %d\n", optval);
    //设置optval
    if (optval == 0)
        optval = 1;

    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, addrlen);
    //查看新的optval
    getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
    printf("SO_REUSEADDR = %d\n", optval);

    memset(&addr, 0, addrlen);
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(9526);

    addrlen = sizeof(addr);
    if (bind(sock, (struct sockaddr*) & addr, addrlen) == -1)
    {
        error_handling("tw_tcp_server bind failed");
    }

    listen(sock, 3);
    client = accept(sock, (struct sockaddr*)&cliAddr, &addrlen);
    read(client, buffer, sizeof(buffer));
    close(client);
    close(sock);
}

void tw_tcp_client()
{
    int client = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    servaddr.sin_port = htons(9526);
    int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
    if (ret == -1) {
        std::cout << "client connect failed!\n";
        close(client);
        return;
    }
    std::cout << "client connect server is success!\n";

    char buffer[256] = "";
    while (ret == 0)
    {
        fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
        fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
        if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
            break;
        }

        size_t len = strlen(buffer);
        size_t send_len = 0;

        //当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
        while (send_len < len)
        {
            ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
            if (ret <= 0) {//连接出了问题
                fputs("may be connect newwork failed,make client write failed!\n", stdout);
                close(client);
                return;
            }
            send_len += (size_t)ret;

            std::cout << "client write success, msg:" << buffer << std::endl;

        }
        memset(buffer, 0, sizeof(buffer));

        //当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
        size_t read_len = 0;
        while (read_len < len)
        {
            size_t ret = read(client, buffer + read_len, len - read_len);
            if (ret <= 0) {//连接出了问题
                fputs("may be connect newwork failed, make client read failed!\n", stdout);
                close(client);
                return;
            }
            read_len += (size_t)ret;
        }
        std::cout << "from server:" << buffer << std::endl;
    };

    close(client);
    std::cout << "client done!" << std::endl;
}

void tw_func(char* option)
{
    if (strcmp(option, "1") == 0)
    {
        tw_tcp_server();
        tw_tcp_server();
    }
    else {
        tw_tcp_client();
    }
}
  • 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
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • main函数中调用:
 tw_func(argv[1]);//Time-wait超时等待
  • 1
  • 运行结果:
    在这里插入图片描述

3,TCP_NODELAY

NODELAY:没有延时。

Nagle算法

Nagle算法是以他的发明人John Nagle的名字命名的,它用于自动连接许多的小缓冲器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率
在这里插入图片描述
从上图中可以得到如下结论:

  • “只有收到前一数据的ACK消息时,Nagle算法才发送下一数据。”
    TCP套接字默认使用Nagle算法交换数据,因此最大限度地进行缓冲,直到收到ACK。上图左侧正是这种情况。为了发送字符串"Nagle",将其传递到输出缓冲。这时头字符"N"之前没有其他数据(没有需接收的ACK),因此立即传输。之后开始等待字符"N"的ACK消息,等待过程中,剩下的"agle"填入输出缓冲。接下来,收到字符"N"的ACK消息后,将输出缓冲的"agle"装入一个数据包发送。也就是说,共需传递4个数据包以传输1个字符串。
  • 接下来分析未使用Nagle算法时发送字符串"Nagle"的过程。假设字符"N"到"e"依序传到输出缓冲。此时的发送过程与ACK接收与否无关,因此数据到达输出缓冲后将立即被发送出去。从上图右侧可以看到,发送字符串"Nagle"时共需10个数据包。由此可知,不使用Nagle算法将对网络流量产生负面影响。即使只传输1个字节的数据,其头信息都有可能是几十个字节。因此,为了提高网络传输效率,必须使用Nagle算法。
  • 在程序中将字符串传给输出缓冲时并不是逐字传递的,故发送字符串"Nagle"的实际情况并非如上图 所示。但如果隔一段时间再把构成字符串的字符传到输出缓冲(如果存在此类数据传递)的话,则有可能产生类似上图的情况。上图中就是隔一段时间向输出缓冲传递待发送数据的。
  • 但Nagle算法并不是什么时候都适用。根据传输数据的特性,网络流量未受太大影响时,不使用Nagle算法要比使用它时传输速度快。最典型的是"传输大文件数据"。将文件数据传入输出缓冲不会花太多时间,因此,即便不使用Nagle算法,也会在装满输出缓冲时传输数据包。这不仅不会增加数据包的数量,反而会在无需等待ACK的前提下连续传输,因此可以大大提高传输速度。
  • 一般情况下,不适用Nagle算法可以提高传输速度。但如果无条件放弃使用Nagle算法,就会增加过多的网络流量,反而会影响传输。因此,未准确判断数据特性时不应禁用Nagle算法
  • 刚才说过的"大文件数据"应禁用Nagle算法。换言之,如果有必要,就应禁用Nagle算法。"Nagle算法使用与否在网络流量上差别不大,使用Nagle算法的传输速度更慢"禁用方法非常简单。
  • 从下列代码也可看出,只需将套接字可选项TCP_NODELAY改为1(真)即可。
int opt_val=1;
setsockopt(sock, IPPROTO_TCP,TCP_NODELAY,(void*)&opt_val, sizeof(opt_val));
  • 1
  • 2
  • 可以通过TCP_NODELAY的值查看Nagle算法的设置状态
int opt_val;socklen_t opt_len;
opt_len=sizeof(opt_val);
getsockopt(sock,IPPROTO_TCP,TCP_NODELAY,(void*)&opt_val,&opt_len);
  • 1
  • 2
  • 3
  • 如果正在使用Nagle算法,opt val变量中会保存0;
    如果已禁用Nagle算法,则保存1。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/631690
推荐阅读
相关标签
  

闽ICP备14008679号