码迷,mamicode.com
首页 > 其他好文 > 详细

网络编程实战2

时间:2020-06-14 20:33:35      阅读:52      评论:0      收藏:0      [点我收藏+]

标签:队列大小   include   描述   mes   传输   address   ++   进制   load   

ctrl+Alt打开terminal,uname -a查看linux内核版本。我这里安装的ubuntu的内核版本为5.4.0-29-generic。

socket.h中只有函数声明,要获得c文件得解压linux内核源码。

extern int socket (int __domain, int __type, int __protocol) __THROW;

函数的作用是创建套接字
__domain就是指PF_INET、PF_INET6以及PF_LOCAL等,表示是什么样的套接字
__type可用的值是

  • SOCK_STREAM:表示的是字节流,对应TCP
  • SOCK_DGRAM:表示的是数据报,对应UDP
  • SOCK_RAW:表示的是原始套接字

__protocol:protocol本来是用来指定通信协议的,但现在基本废弃,因为协议已经通过前面两个参数指定完成,protocol目前一般写成0即可。
返回文件描述符,异常返回-1

这篇文章的预处理指令写的很细致。https://www.zfl9.com/c-cpp.html
extern 只在头文件中做声明,否则两个文件都引用同一个头文件时,会出现重复定义的问题。
在C语言中int a;即使没赋值也算定义了。在头文件test.h中声明extern int a;另一个c文件test1.c中定义int a = 100;然后在test2.c中,引入头文件test.h,主函数中打印a,可以获得test1.c中定义的a的值。
注意在clion中测试的时候,test1.c和test2.c要属于同一个target,且只有一个主函数,否则会发生链接错误。
变量名前两个下划线:涉及到命名规则。一个下划线加大写字母,两个下划线,都是给编译器和标准库用的。而我们一般只用一个下划线加小写字母表示私有变量。命名时最好不要使用下划线开头。以免发生冲突。
__THROW指什么?#define __THROW attribute ((nothrow __LEAF))。它是一个宏。知乎上的解释。https://zhuanlan.zhihu.com/p/123879953 C++会调用C函数,它控制当C++代码调用这段C函数的行为。nothrow告诉编译器这个函数不会扔出异常,leaf告诉编译器这个函数传进来的参数不会进行修改。在C语言里,这段代码没有意义,只有C++调用时才有意义。且这个宏只在linux的C库里有。至于__attribute__暂时就不细深究了。

网络编程中客户端与服务端核心逻辑
技术图片

发送缓冲区概念
技术图片

TCP三次握手
技术图片

发送缓冲区和接收缓冲区实验
client.c

//
// Created by tobin on 6/13/20.
//

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

#define MESSAGE_SIZE 1024000
#define SERVER_PORT 12345
#define SERVER_ADDR "127.0.0.1" // localhost 本机ip

void send_data(int);

int main(int argc, char **argv) {
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVER_PORT); // 主机字节序转为网络字节序,网络字节序大端,主机字节序,intel是小端,arm tcp/ip都是大端
    // 网络通信中一般只有端口号和ip地址进行大小端的转换,其他数据是以字符串的格式传输,所以无需转换
    inet_pton(AF_INET, SERVER_ADDR, &servaddr.sin_addr);
    // 将点分十进制的ip地址转为用于网络传输的数值格式,存于sin_addr中,详细可以参考 https://blog.csdn.net/zyy617532750/article/details/58595700
    connect(sockfd, (const struct sockaddr *) &servaddr, sizeof(servaddr)); // 连接服务端
    send_data(sockfd);
    exit(0); //https://blog.csdn.net/song_esther/article/details/85707459 exit是进程的结束,操作系统提供的系统调用,和rentun 0,在主函数中区别不大
}

void send_data(int sockfd) {
    char *query;
    query = malloc(MESSAGE_SIZE + 1); // 分配内存,单位是字节
    for (int i = 0; i < MESSAGE_SIZE; i++) {
        query[i] = ‘a‘;
    }
    query[MESSAGE_SIZE] = ‘\0‘;

    const char *cp;
    cp = query;
    long remaining = (long) strlen(query);
    while (remaining) {
        long n_written = send(sockfd, cp, remaining, 0); // flags = 0 ,send等价于write, cp是要发送的消息,remaining是要发送的字节数
        // 阻塞套接字,n_written就是需要发送的数据大小,即循环只运行一次
        // send返回只表示数据发送到发送缓冲区当中,接收方需要循环读取数据,并考虑EOF等异常条件
        fprintf(stdout, "send into buffer %ld \n", n_written);
        if (n_written < 0) {
            perror("send");
            return;
        }
        remaining -= n_written;
        cp += n_written;
    }
}

server.c

//
// Created by tobin on 6/12/20.
//

#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
// #include <zconf.h>
#include <errno.h>

#define SERVER_PORT 12345

ssize_t readn(int, void *, size_t); //ssize_t 有符号整型,在32位机上等于int,在64位机上等于long int

void read_data(int);

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t clilen; // unsigned int
    struct sockaddr_in cliaddr, servaddr; // 这里是socket地址,具体内容复制在下面
    //struct sockaddr_in
    // {
    // __SOCKADDR_COMMON (sin_); // 地址族,比如ipv6 ipv4就是常见的因特网地址族
    // /* #define	__SOCKADDR_COMMON(sa_prefix)     // sa_family_t sa_prefix##family */ // 使用了宏定义,且其中有##的特殊用法,相当于sa_prefixfamily,连接符号,#str,则是在前后加双引号,转为字符串
    // in_port_t sin_port;	/* Port number. */ // 无符号整数,16位端口号
    // struct in_addr sin_addr;	/* Internet address. */ // 32位无符号ip地址
    //
    // /* Pad to size of `struct sockaddr‘. */
    // 填充字段,保证sockaddr_in(因特网套接字)的大小和sockaddr(通用套接字相同),通用套接字大小为128位
    // unsigned char sin_zero[sizeof (struct sockaddr)
    //	- __SOCKADDR_COMMON_SIZE
    //	- sizeof (in_port_t)
    //	- sizeof (struct in_addr)];
    // };
    //

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));// 不是标准C函数,gcc支持,功能相当于memset(&servaddr, 0, sizeof(servaddr)),清零
    // extern void bzero (void *__s, size_t __n) __THROW __nonnull ((1)); void* 可以修饰不确定类型的指针,但是void不能修饰变量
    servaddr.sin_family = AF_INET; // ipV4
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY,服务器地址绑定到默认网卡地址(0x00000000)
    servaddr.sin_port = htons(SERVER_PORT); // 设置服务器端口地址,服务器地址一般是绑定在互相都知道的端口上,如果传入0,则操作系统分配空闲端口

    bind(listenfd, (const struct sockaddr *) &servaddr, sizeof(servaddr));
    // 当时还没有void *,设计了通用socket地址,将服务器地址转为通用socket地址,这里const指针,即bind函数内部不能通过传进来的servaddr指针去修改内容
    // 套接字和地址相关联,此时的套接字是主动套接字
    // int a;
    // const int *p1 = &a;//p1是指向常量的指针
    // int b;
    // p1 = &b;//正确,p1本身的值可以改变
    // *p1 = 1;//编译出错,不能通过p1改变所指对象
    // int a;
    // const int *p1 = &a;//p1是指向常量的指针
    // int b;
    // p1 = &b;//正确,p1本身的值可以改变
    // *p1 = 1;//编译出错,不能通过p1改变所指对象

    listen(listenfd, 1024); // 主动套接字变成被动套接字,监听,第二个参数表示已完成建立(ESTABLISHED)但是未Accept的队列大小,决定了并发数目


    for (;;) {
        clilen = sizeof(cliaddr);
        connfd = accept(listenfd, (const struct sockaddr *) &cliaddr, &clilen);
        // 返回新的fd,表示客户端与服务端的连接,监听套接字即listenfd一直在工作,这样就可以处理多个用户的请求
        read_data(connfd);

        close(connfd); // 关闭连接
    }
    return 0;
}

void read_data(int sockfd) {
    ssize_t n;
    char buf[1024];

    int time = 0;
    for (;;) {
        fprintf(stdout, "block in read\n");
// 都是把格式好的字符串输出,只是输出的目标不一样:
// 1 printf,是把格式字符串输出到到标准输出(一般是屏幕,可以重定向)。
// 2 sprintf,是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。
// 3 fprintf, 是把格式字符串输出到指定文件设备中,所以参数笔printf多一个文件指针FILE*
        if ((n = readn(sockfd, (void *) buf, (size_t) 1024)) == 0) // 如果返回0,说明对方发了FIN包
            return;
        time++;
        fprintf(stdout, "1K read for %d \n", time);
        sleep(1); // 睡眠1s,模拟服务器时延
    }
}

// vptr 是buffer数组,size是每次读多少字节的数据,返回的是真实读了多少数据,有可能读的过程中,发了FIN包
ssize_t readn(int fd, void *vptr, size_t size) {
    size_t nleft;
    ssize_t nread;
    char *ptr;

    ptr = vptr;
    nleft = size;

    while (nleft > 0) {
        if ((nread = read(fd, ptr, nleft)) < 0) { //
            //read 函数要求操作系统内核从套接字描述字 socketfd读取最多多少个字节(size),并将结果存储到 buffer 中。
            // 返回值告诉我们实际读取的字节数目,也有一些特殊情况,如果返回值为 0,表示 EOF(end-of-file),
            // 这在网络中表示对端发送了 FIN 包,要处理断连的情况;如果返回值为 -1,表示出错。当然,如果是非阻塞 I/O,情况会略有不同
            if (errno == EINTR) // 系统中断,客户端发送了FIN包,则停止读数据
                nread = 0;
            else
                return (-1);
        } else if (nread == 0) { // 停止读数据
            break;
        }

        nleft -= nread; // 剩余未读数据
        ptr += nread; // buffer数组指针王往后移动
    }
    return size - nleft; // 返回此次所读数据的字节个数
}

server运行结果
技术图片

client运行结果
技术图片

拓展问题:
发送缓冲区越大越好吗?内核缓冲区总是充满数据会发生粘包问题。同时网络的传输大小会限制单次发送的大小,最后由于数据堵塞需要消耗大量的内存资源,资源利用率不高
数据从应用程序发送端到应用程序接收端,复制了几次。
复制几次没有固定答案。 下面来源于极客时间的讲解。
技术图片

让我们先看发送端,当应用程序将数据发送到发送缓冲区时,调用的是 send 或 write 方法,如果缓存中没有空间,系统调用就会失败或者阻塞。我们说,这个动作事实上是一次“显式拷贝”。而在这之后,数据将会按照 TCP/IP 的分层再次进行拷贝,这层的拷贝对我们来说就不是显式的了。
接下来轮到 TCP 协议栈工作,创建 Packet 报文,并把报文发送到传输队列中(qdisc),传输队列是一个典型的 FIFO 队列,队列的最大值可以通过 ifconfig 命令输出的 txqueuelen 来查看。通常情况下,这个值有几千报文大小。
TX ring 在网络驱动和网卡之间,也是一个传输请求的队列。
网卡作为物理设备工作在物理层,主要工作是把要发送的报文保存到内部的缓存中,并发送出去。
接下来再看接收端,报文首先到达网卡,由网卡保存在自己的接收缓存中,接下来报文被发送至网络驱动和网卡之间的 RX ring,网络驱动从 RX ring 获取报文 ,然后把报文发送到上层。
这里值得注意的是,网络驱动和上层之间没有缓存,因为网络驱动使用 Napi 进行数据传输。因此,可以认为上层直接从 RX ring 中读取报文。
最后,报文的数据保存在套接字接收缓存中,应用程序从套接字接收缓存中读取数据。
这就是数据流从应用程序发送端,一直到应用程序接收端的整个过程,你看懂了吗?
上面的任何一个环节稍有积压,都会对程序性能产生影响。但好消息是,内核和网络设备供应商已经帮我们把一切都打点好了,我们看到和用到的,其实只是冰山上的一角而已。

网络编程实战2

标签:队列大小   include   描述   mes   传输   address   ++   进制   load   

原文地址:https://www.cnblogs.com/zuotongbin/p/13126384.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!