ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

网络编程实战2

2020-06-14 19:08:35  阅读:266  来源: 互联网

标签:实战 __ int void 编程 网络 接字 include servaddr


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 中读取报文。
最后,报文的数据保存在套接字接收缓存中,应用程序从套接字接收缓存中读取数据。
这就是数据流从应用程序发送端,一直到应用程序接收端的整个过程,你看懂了吗?
上面的任何一个环节稍有积压,都会对程序性能产生影响。但好消息是,内核和网络设备供应商已经帮我们把一切都打点好了,我们看到和用到的,其实只是冰山上的一角而已。

标签:实战,__,int,void,编程,网络,接字,include,servaddr
来源: https://www.cnblogs.com/zuotongbin/p/13126384.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有