Socket网络编程的一切
本文最后更新于 486 天前,其中的信息可能已经有所发展或是发生改变。

TCP,UDP

TCP

特点:

  1. 面向连接
  2. 超时重传
  3. 拥塞控制

TCP三次握手

TCP四次挥手

UDP

特点:

  1. 无连接
  2. 不可靠

使用udp的服务:DNS,SNMP这些场景对延时、丢包不敏感。

Socket

socket是网络编程中绕不开的一个话题,最早由伯克利大学的研究员提出,也称伯克利套接字。它可以视为一个用于屏蔽底层协议栈的接口。

下图是socket网络编程经典的Client/Server模型。

TCP模型

UDP模型

我们可以将整个socket网络的数据传输想象成打电话。打电话就需要有电话机,需要安装电话线,有了电话机装好电话线,我们还需要购买一个电话号码,这样别人才知道怎么能找到你。对比socket的api,bind就像是去电信公司开户,listen就是听到了电话铃声,accept就是拿起电话开始接听。

socket apisocket编程含义现实世界
socket创建socket安装电话机
bind绑定端口开户,设定电话号码
listen监听描述符听到电话响铃
connect建立连接打通电话
accept接受数据收听
send/sendto发送数据说话
recv/recvfrom接收数据收听
close/shutdown关闭描述符关闭文件
  1. Socket API概述

socket – 创建套接字

int socket(int domain, int type, int protocol);
  • domain 指代PF_INETPF_INET6 以及 PF_LOCAL 等,表示要创建何种套接字
  • type 的可用值
    • SOCK_STREAM: 字节流,对应 TCP
    • SOCK_DGRAM:数据报,对应 UDP
    • SOCK_RAW: 原始套接字
  • Protocol 通信协议,depreciated,一般填写0

bind – 绑定电话号码

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:socket创建出的文件描述符
  • addr:通用地址,注意这里的类型本身应该是void*,只是socket创建时c还不支持void*类型。bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析,len 字段表示的就是传入的地址长度,它是一个可变值
  • addrlen:地址长度

对于使用者来说,每次传参的时候我们都需要把ipv4,ipv6或者本地套接字转化为通用套接字的格式,如下:

struct sockaddr_in addr;
bind(sock, (struct sockaddr*)&addr, sizeof(addr));

故而我们可以把bind的参数理解成 void*, 只是BSD设计socket的时候(1982年), 那时C还没有void*的支持。

listen – 接上电话线

bind函数让我们的套接字和地址关联,但是要让别人打通电话,我们还需要接上电话线,让服务处于可接听的状态。

int listen(int listensockfd, int backlog);
  • sockfd: 套接字描述符
  • backlog: 表示已完成(ESTABLISHED)且未accept的队列大小。
    • 这个参数的大小决定了可以接收的并发数目。这个参数越大,理论上并发数目也越大。但是参数过大 -> 系统资源消耗多。对于Linux系统来说,不允许对这个参数进行改变。

accept – 电话铃响

int accept(int listensockfd, struct sockaddr *_Nullable restrict addr,
        socklen_t *_Nullable restrict addrlen);
  • sockfd: listen套接字,这是通过bind,listen一系列操作得到的fd
  • addr: 指向客户端地址的指针
  • addrlen: 地址的大小

要注意有两个套接字,第一个是监听套接字listensockfd,其作为输入参数存在;第二个是返回的已连接套接字描述字。

connect – 拨打电话

上述的api为服务器api,现在我们来看看客户端的api。客户端和服务器端的链接建立,是通过connect函数完成的。这是connect的构建函数:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfdfd: 由socket函数创建
  • addr: 指向套接字地址的结构
  • addrlen: 套接字地址结构大小

客户端在调用函数connect前不必非得调用bind,若需要的话,内核会确定源ip地址,并按照一定的算法选择一个临时端口作为源端口。

若为tcp套接字,那么调用connect函数将激发TCP三次握手的过程,而且只在连接建立成功或者出错的时候才返回。其中出错返回可能有以下几种情况:

  1. 三次握手无法建立,客户端发出的SYN包没有任何响应,于是返回TIMEOUT错误。此类错误的常见原因是对应的服务端IP写错。
  2. 客户端收到了RST应答,此时客户端会立即返回CONNECTION REFUSED错误。这种情况常见于客户端发送连接请求时端口写错。因为 RST 是TCP在发生错误时发送的一种TCP分节。
    1. 产生RST的 三个条件:
      1. 目的地为某端口的SYN到达,但是该端口未被监听
      2. TCP想取消一个已有的连接
      3. TCP接收到一个根本不存在的连接上的分节
  3. 客户端发出的SYN包在网络上返回了destination unreachable, 即目的不可达错误。此类错误的常见原因是路由不通。
  4. 收发缓冲区

客户端在发送数据时可选择如下四个函数:

ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
  1. write是常见的文件写函数,如果fd换成常用的文件描述符,就是普通的写文件函数。
  2. send 用于tcp流发送数据,sendto用于udp发送数据。可指定选项发送带外数据。
  3. sendmsg 指定多重缓冲区传输数据,以结构体msghdr的方式发送数据。

以上函数的使用场景不同。

缓冲区大小

当TCP三次握手成功,TCP连接建立后,os kernel 会为每一个连接创建配套的设置,如发送缓冲区。

在linux系统中发送缓冲区的大小受以下选项和行为控制:

  1. 由kernel控制,缓冲区的最小值,默认值和最大值:
cat /proc/sys/net/ipv4/tcp_wmem
4096    16384   4194304
  1. 由kernel控制,缓冲区的最大值:
cat  /proc/sys/net/core/wmem_max
212992
  1. 由kernel控制,缓冲区的默认值
cat /proc/sys/net/core/wmem_default
212992
  1. 在socket选项中,可通过setsockopt设置缓冲区大小为n
int send_buf_size = 20 * 1024 * 1024;
setsockopt(fd, SOL_SOCKET, SO_SEND_BUF, &set_buf_size, sizeof(int));

要注意的是:

通过setsockopt设置缓冲区大小为n,但是最大大小受/proc/sys/net/core/wmem_max控制。且实际上我们发现getsockopt获取的缓冲区大小为2*n,这是基于overhead解包的需求,会多预留空间。

源码的处理:

case so_sendbuf:
if (val > sysctl_wmem_max)
        val = stsctl_wmen_max;
if ((val * 2) < SOCK_MIN_SENDBUF)
        sk->sk_sndbuf = SOCK_MIN_SENDBUF;
else
        sk->sk_sendbuf = val * 2;

写入数据

发送缓冲区的大小可以通过以上的设置改变,当我们的程序调用写入函数时,实际上做的事是将数据从user space拷贝到os kernel中的发送缓冲区中,而非将数据通过socket传出去。后续的部分由内核协议栈完成。

在写入缓冲区的时候有两种情况(非阻塞情况下)

  1. 内核缓冲区足够大,足以容纳所要发送的数据,那么自然返回成功。
  2. 要发送的数据长度超过了当下内核缓冲区的空闲大小,那么这个时候send函数会返回已成功拷贝到缓冲区中的字节数。同时errno会被置为EAGAINorEWOULDBLOCK

读取数据

让我们从最简单的read函数开始

ssize_t read(int fd, void *buffer, size_t size);

read函数要求os kernel从套接字fd读取最多size个字节,并将结果储存到buffer中。返回值告诉我们实际读取的字节数目。

特殊情况:

  1. ret == 0; 表示EOF(end of file), 这在网络中表示对端发送了FIN包,要处理断连的情况
  2. ret == -1; 表示出错。

相关问题

socket部分

  1. socket默认为阻塞调用,既然有阻塞调用,那么就有非阻塞调用。那么如何使用非阻塞调用呢?使用的场景在哪里呢?

答:非阻塞的场景:高性能服务器编程。所有的调用都不需要等待对方准备好了再返回,而是立即返回。那么怎么知道是否准备好了?

就是将fd注册到类似select或者epoll这样的io多路复用机制中,变多个fd阻塞为一个fd阻塞。那么只要有任何一个fd准备好了,select或者epoll都会返回。我们再从中取出fd做对应的操作即可。

  1. 客户端在发起connect调用之前,可以调用bind函数吗?

答:客户端可以bind指定固定端口来建立连接。

若没有bind则会产生一个随机的端口来完成连接请求。

  1. udp是否可以使用connect? udp使用connect和tcp有何不同?

答:udp可以使用connect。tcp协议使用connect是为了进行三次握手,而udp协议使用connect实际上不会向对端发送任何报文,而只是在本机建立起四元组的上下文。udp在使用connect后可以使用send来代替sendto,这是因为四元组是上下文已经建立,若未使用connect,则需要使用sendto显式指定对端的ip和port。

  1. write函数传入普通文件描述符和套接字描述符有什么区别?

答:1)对于普通文件描述符而言,一个fd代表一个打开了的文件句柄。通过调用write函数,os kernel帮我们往文件系统中不断地写入字节流。通常而言,写入的字节流大小和输入参数size的值是相等的,否则表示出错。

2)而对于套接字fd来说,它代表一个双向的连接,在套接字fd上调用write写入的字节数有可能比我们申请的要少(在缓冲区满的情况下)。

  1. udp单次传输的最大数据量是多少?

答:我们知道ip封包的总长度为两字节,即一个ip包最大长度为65535,那么减去一个ip头(20字节),减去一个udp头(8字节)= 65507字节。

  1. 在写入发送缓冲区时若缓冲区若数据为全部拷贝,如何解决发送数据不完整的问题?(即所谓半包问题)

答:由于我们已知成功拷贝到缓冲区的字节数,一个较好的处理方式是将剩余未拷贝的数据保存下来,然后监控该fd直到下次可写状态到来。再将其写入缓冲区。

  1. 能否把内核发送缓冲区的大小无限调大?

答:不行。

  1. 网卡一次发出去的数据表肯定也有一个最大长度,所以无论累计再多数据最后都是要分片发送的。
  2. 数据传输有时延要求,缓冲区里如果有数据的话也会持续进行消费,将缓冲区设置的太大反而浪费系统资源。
  1. 一段数据流从应用程序发送端,一直到应用程序接收端,总共经过多少次拷贝?

答:8次。主要体会tcp/ip分层的思想。

TCP协议部分

  1. 简单说说,为什么tcp连接需要三次握手?

答:本质上是因为tcp连接的双方都要确认各自收发消息的能力是正常的

Client -- syn --> Server: 这是客户端第一次发送syn消息到服务端,此时client可以确认自己有发消息的能力。

Clinet <-- syn, ack -- Server: 服务器发送sync和ack到,此时server确认自己有收,发消息的能力,还需确认client有收消息的能力。

Clinet -- ack --> Server: 客户端确认自己有接收的能力。

  1. 如果握手过程中客户端的第三次应答,服务器没有收到,然后客户端开始发消息给服务器,这时候服务器和客户端的表现是什么?客户端会收到什么返回?

答:如果三次应答服务器没有收到,服务端连接没有建立,客户端连接建立了,发送的报文会被设置为连接RST

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇