文章目录

  • 一 简略了解网络
    • 1 三种socket
      • 1.1 SOCK_STREAM
      • 1.2 SOCK_DGRAM
      • 1.3 SOCK_RAW
    • 2 面向连接和无连接
      • 2.1 面向连接
      • 2.2 无连接
    • 3 网络模型OSI与TCP/IP
      • 3.1 OSI
      • 3.2 TCP/IP
        • 3.2.1 用于各层常见的协议
    • 4 网络地址
      • 4.1 IP地址
      • 4.2 MAC地址
      • 4.3 端口号
    • 5 字节序及转换
      • 5.1 网络字节序为大端序
      • 5.2 用于端口号的转换函数
      • 5.3 用于IP地址的转换函数
  • 二 socket编程接口
    • 1 socket()
    • 2 bind()
    • 3 listen()
    • 4 accept()
    • 5 connect()
    • 6 close()与shutdown()
    • 7 读写数据
      • 7.1 文件IO中的read()/write()
      • 7.2 send()
      • 7.3 recv()
  • 三 TCP和UDP模型
    • 1 TCP模型
      • 1.1 三次握手
      • 1.2 数据传输
        • 1.2.1 正常传输:
        • 1.2.2 传输异常时的处理
      • 1.3 四次握手断开
      • 1.4 关于 TIME_WAIT 状态的说明
      • 1.5 建议shutdown和close一起用而不是只用close
    • 2 UDP模型
  • 四 IO多路复用
    • 1 select()
    • 2 poll()
    • 3 epoll()
  • 五 设置socket属性
  • 六 广播与组播
    • 1 广播
      • 1.1 广播地址:
      • 1.2 广播编程步骤:
    • 2 组播
      • 2.1 网络地址分类
      • 2.2 组播编程步骤
  • 七 UNIX域套接字
  • 内容参考来源

socket原意插座,是互联网中的插座;(套接字?怒喷翻译)
插头插到插座上就能从电网获得电力供应;为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具;

一 简略了解网络

如果你想看编程接口,请不要看这章,本章内容繁杂直接劝退;

1 三种socket

socket有流式SOCK_STREAM和数据报SOCK_DGRAM两种,前者用于TCP模型,后者用于UDP模型;

1.1 SOCK_STREAM

是一种可靠的、双向的通信数据流,TCP协议能够使数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送;
SOCK_STREAM 有以下几个特征:
数据在传输过程中不会消失;
数据是按照顺序传输的;
数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。

1.2 SOCK_DGRAM

数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字
计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。
因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。
SOCK_DGRAM 有以下特征:
强调快速传输而非传输顺序;
传输的数据可能丢失也可能损毁;
限制每次传输的数据大小;
数据的发送和接收是同步的(有的教程也称“存在数据边界”)。

1.3 SOCK_RAW

可以对较低层次协议如IP、ICMP直接访问;

2 面向连接和无连接

面向连接和无连接指的都是协议。也就是说,这些术语指的并不是物理介质本身,而是用来说明如何在物理介质上传输数据的。面向连接和无连接协议可以,而且通常也确实会共享同一条物理介质。

2.1 面向连接

面向连接的协议则维护了分组之间的状态,使用这种协议的应用程序通常都会进行长期的对话。记住这些状态,协议就可以提供可靠的传输。
典型的面向连接协议有三个阶段。第一阶段,在对等实体间建立连接。接下来是数据传输阶段,在这个阶段中,数据在对等实体间传输。最后,当对等实体完成数据传输时,连接被拆除。
有连接socket非常可靠,万无一失,但是传输效率低,耗费资源多

2.2 无连接

无连接协议中的分组被称为数据报(datagram),每个分组都是独立寻址,并由应用程序发送的。从协议的角度来看,每个数据报都是一个独立的实体,与在两个相同的对等实体之间传送的任何其他数据报都没有关系,这就意味着协议很可能是不可靠的。也就是说,网络会尽最大努力传送每一个数据报,但并不保证数据报不丢失、不延迟或者不错序传输。
无连接socket传输效率高,但是不可靠,有丢失数据包、捣乱数据的风险

3 网络模型OSI与TCP/IP

3.1 OSI

Open System Interconnection 的缩写,译为“开放式系统互联”。

我找到了这篇文章:OSI七层模型传输过程的通俗理解
看完我就懂了每层到底在干嘛。现在我把文章中每层的作用整理在此:

  • 应用层:用户通过应用程序向应用层提出请求,比如发送邮件、访问网页,在这一层用到确定发送方式,比如邮件选择STMP或者IMAP,网页请求是HTTP还是HTTPS。
  • 表示层:准备数据,把格式和编码变成网路传输中通用的形式;
  • 会话层:一个进程的传输请求就是一个会话,会话层负责建立,维护,断开每个用户进程与各个服务器的连接;
  • 传输层:在建立好的连接中,把表示好的数据进行合并,编号,发送;并处理各种传输中的异常,保证传输可靠性;
  • 网络层:计算出主机局域网与目标服务器所在局域网的最佳线路并把数据发送到目标局域网;
  • 数据链路层:通过MAC地址在局域网中找到目标服务器;
  • 物理层:信号转换和物理传输,将数据转换成光信号、电信号,然后通过光缆、铜缆传输。

也就是说,(1)用户通过应用层传入数据和数据协议,(2)表示层把本地数据转换成网络通用格式,(3)会话层建立和维护与服务器的连接,(4)传输层控制数据发送方式,解决数据发送异常,确保传输可靠性,(5)网络层把传输层的数据送到服务器所在局域网,(6)数据链路层在局域网内找到服务器,(7)物理层是中间的光缆等物理线路;

还要注意:两台计算机进行通信时,必须遵守以下原则:

  • 必须是同一层次进行通信,比如,A 计算机的应用层和 B 计算机的传输层就不能通信,因为它们不在一个层次,数据的拆包会遇到问题。
  • 每一层的功能都必须相同,也就是拥有完全相同的网络模型。如果网络模型都不同,那不就乱套了,谁都不认识谁。
  • 数据只能逐层传输,不能跃层。
  • 每一层可以使用下层提供的服务,并向上层提供服务。

3.2 TCP/IP

OSI的上层应用层,表示层,会话层统一为应用层;数据链路层和物理层统一为网络接口层;
OSI复杂且多,放到现实中无法实现;
TCP/IP可以且好实现,风靡现实世界;

3.2.1 用于各层常见的协议

  • 应用层:Telnet、FTP、HTTP、DNS、SMTP;
  • 传输层:TCP、UDP;
  • 网络层:IP、ICMP、IGMP;
  • 网络接口层:以太网、令牌环网、FDDI等;

4 网络地址

4.1 IP地址

Internet Protocol Address 的缩写,译为“网际协议地址”;
有IPv4和IPv6两种,IPv4是32位,IPv6是128位;
表示有点分十进制的字符串表示法和32位二进制表示法;比如"192.168.4.1"和0b11111111101010100101010110101010;(随便写的,不要拿去计算),编程时使用点分十进制,在网络传输前要转换为二进制;

4.2 MAC地址

Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。

真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了;。局域网中的交换机会记录每台计算机的 MAC 地址。
数据链路层,当数据已经到达目标路由器所在的局域网,数据转给交换机,由交换机根据MAC地址找到目标主机;

4.3 端口号

一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号

5 字节序及转换

5.1 网络字节序为大端序

表示层将用户的数据转换为网络传输中的同意通用格式,字节序就是一种;
首先,字节序本身只有两种,大端序和小端序;
大端序Big-Endian:低地址放的是高字节;
小端序Little-Endian:低地址放的是低字节;
在网络和本地主机通讯期间,就分出了网络字节序和主机字节序;
网络字节序NBO - Network Byte Order:使用统一的字节顺序,避免兼容性问题;
主机字节序HBO - Host Byte Order:不同的机器HBO是不一样的,这与CPU的设计有关;
网络字节序为大端序;在数据传输之前,需要把数据都转成大端序;

Linux库函数中提供了:

5.2 用于端口号的转换函数

主机字节序到网络字节序:

u_long htonl (u_long hostlong);
u_short htons (u_short short);

网络字节序到主机字节序:

u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);

5.3 用于IP地址的转换函数

inet_aton( )将strptr所指的字符串转换成32位的网络字节序二进制值

#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);

inet_addr( )功能同上,返回转换后的地址。

int_addr_t inet_addr(const char *strptr);

inet_ntoa( )将32位网络字节序二进制地址转换成点分十进制的字符串。

char *inet_ntoa(stuct in_addr inaddr);

inet_pton()将IPV4/IPV6的地址转换成binary格式

int inet_pton(int af, const char *src, void *dst);

二 socket编程接口

socket通信流程:

socket() 创建套接字
bind() 绑定本机地址和端口
connect() 建立连接
listen() 设置监听端口
accept() 接受TCP连接
recv(), read(), recvfrom() 数据接收
send(), write(), sendto() 数据发送
close(), shutdown() 关闭套接字

各函数详解:

1 socket()

创建一个socket,以文件形式,并返回其描述符;这个描述符就是此进程的socket fd
;在服务器端为服务器socket fd,在客户端为客户端socket fd;

#include <sys/types.h>
#include <sys/socket.h>
int socket (int domain, int type, int protocol);

成功返回一个描述符,失败返回-1;并设置errno;

  • domain 是地址族
    AF_INET // internet 协议
    AF_UNIX // unix internal协议
    AF_NS // Xerox NS协议
    AF_IMPLINK // Interface Message协议
  • type // 套接字类型
    SOCK_STREAM // 流式套接字- TCP
    SOCK_DGRAM // 数据报套接字- UDP
    SOCK_RAW // 原始套接字- 直接传输
  • protocol 参数通常置为0,表示系统自动匹配;
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

2 bind()

服务器端专用函数,把一个地址和端口号放在结构体里然后绑定给一个socket fd;用于服务器端,使服务器获得IP地址和端口号;

#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd, struct sockaddr* addr, int addrLen);

成功返回0,失败返回-1;并设置errno;

  • sockfd 由socket() 调用返回
  • addr 是指向 sockaddr 结构的指针,包含本机IP 地址和端口号
  • addrlen是addr的大小;一般写成sizeof(struct sockaddr)

下面是通用地址结构sockaddr结构体,总共是16字节大小:

struct sockaddr
{    u_short  sa_family;  // 地址族, AF_xxxchar  sa_data[14];   // 14字节协议地址,
};

sa_data[14]包含了IP地址和端口号,转换为点分十进制比如:“192.168.4.12:8888”;
但是我们没有直接处理14字节数据的函数,只能用下面这个结构体作为中间工具人变量,
Internet协议地址结构:

struct sockaddr_in{           u_short sin_family;    // 地址族, AF_INET,2 bytesu_short sin_port;      // 端口,2 bytesstruct in_addr sin_addr; // IPV4地址,4 bytes  char sin_zero[8];      // 8 bytes unused,作为填充};
// internet address
struct in_addr
{in_addr_t  s_addr;  // u32 network address
};

用法如下:

(1)定义一个struct sockaddr_in类型的变量并清空struct sockaddr_in myaddr;memset(&myaddr, 0, sizeof(myaddr));
(2)填充地址信息myaddr.sin_family = PF_INET;myaddr.sin_port = htons(8888); myaddr.sin_addr.s_addr = inet_addr(“192.168.1.100”);
(3)将该变量强制转换为struct sockaddr类型在函数中使用bind(listenfd, (struct sockaddr*)(&myaddr), sizeof(struct sockaddr));

3 listen()

服务器端专用函数,使socket处于监听状态,此后,当有客户端请求连接时,socket会处理请求,使进程从等待连接的函数阻塞中返回;listen并不会使socket阻塞,accept才会;

#include <sys/types.h>
#include <sys/socket.h>
int listen (int sockfd, int backlog);

成功返回0,失败返回-1;并设置errno;

  • sockfd: 监听连接的套接字
  • backlog:指定了正在等待连接的最大队列长度,它的作用在于处理可能同时出现的几个连接请求。

4 accept()

该函数用在listen()之后,使进程阻塞,当客户端请求连接,并建立连接之后,函数返回连接好的socket fd,这个socket fd是专门与客户端通信,每有一个客户端连接,就会从一个accept()函数返回一个新的socket fd;
并且,第二个参数将返回客户端的地址结构体,参数三返回这个结构体的大小,所以要用这个两个参数接收客户端的信息就要先把地址结构体和存放结构体大小的变量定义出来,然后把它们的地址当作指针传入;

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;

连接成功返回已建立好连接的套接字,失败返回-1并设置errno;

  • sockfd : 监听套接字
  • addr : 接收对方信息的地址结构体的地址
  • addrlen:接收对方信息结构体大小的变量的地址

5 connect()

客户端专用函数,客户端只需要socket()出自己的socket fd,之后直接用自己的fd去连接服务器,传输数据也是用自己的fd;

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

返回值:成功返回0,失败返回 -1并设置errno;

  • sockfd : socket返回的文件描述符
  • serv_addr : 服务器端的地址信息
  • addrlen : serv_addr的长度

6 close()与shutdown()

#include <unistd.h>
int close(int sockfd); //关闭双向通讯
#include <sys/socket.h>
int shutdown(int sockfd, int howto);//有选择关闭

TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown。
针对不同的howto,系统回采取不同的关闭方式。
howto = 0
关闭读通道,但是可以继续往套接字写数据。
howto = 1
和上面相反,关闭写通道。只能从套接字读取数据。
howto = 2
关闭读写通道,和close()一样

7 读写数据

7.1 文件IO中的read()/write()

既然是fd,那就可以用文件IO;

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

read()和write()经常会代替recv()和send();
使用read()/write()和recv()/send()时最好统一;

7.2 send()

#include <sys/socket.h>
ssize_t  send(int socket, const void *buffer, size_t length, int flags);

成功返回实际发送的字节数,失败返回-1, 并设置errno;

  • buffer : 发送缓冲区首地址
  • length : 发送的字节数
  • flags : 发送方式(通常为0),一般填写0,此时和write()作用一样;
    特殊的标志:
    MSG_DONTWAIT: Enables nonblocking operation; 非阻塞版本
    MSG_OOB:用于发送TCP类型的带外数据(out-of-band)

7.3 recv()

#include <sys/socket.h>
ssize_t  recv(int socket, const void *buffer, size_t length, int flags);

成功返回实际接收的字节数,失败返回-1, 并设置errno;

  • buffer : 发送缓冲区首地址
  • length : 发送的字节数
  • flags : 接收方式(通常为0)
    MSG_DONTWAIT: Enables nonblocking operation; 非阻塞版本
    MSG_OOB:用于发送TCP类型的带外数据(out-of-band)
    MSG_PEEK:接收消息的开头,但不删除该消息数据。 因此,随后的接收将
    返回相同的数据;

程序示例请看【Linux】网络篇二–TCP编程

三 TCP和UDP模型

1 TCP模型

The Transmission Control Protocol,传输控制协议
首先看TCP数据报的格式:

带阴影的几个字段需要重点说明一下:

  • 序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
  • 确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。其中seq是上一次收到数据的序号;
  • 标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:
    (1) URG:紧急指针(urgent pointer)有效。
    (2) ACK:确认序号有效。
    (3) PSH:接收方应该尽快将这个报文交给应用层。
    (4) RST:重置连接。
    (5) SYN:建立一个新连接。
    (6) FIN:断开一个连接。
    对英文字母缩写的总结:Seq 是 Sequence 的缩写,表示序列;Ack(ACK) 是 Acknowledge 的缩写,表示确认;SYN 是 Synchronous 的缩写,愿意是“同步的”,这里表示建立同步连接;FIN 是 Finish 的缩写,表示完成。

1.1 三次握手

三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。
当客户端开始发起连接请求:
(1) 当客户端调用 connect() 函数后,TCP协议组建一个数据包,设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。
(2) 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。
(服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系)
服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。
服务器将数据包发出,进入SYN-RECV状态。
(3) 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。
接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。
客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。
(4) 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。
至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

三次握手:
客户端:连吗?
服务器:连。
客户端:好嘞。

1.2 数据传输

连接成功后,就可以互传数据了,为了保证数据准确到达,目标机器在收到数据包(包括SYN包、FIN包、普通数据包等)包后必须立即回传ACK包,这样发送方才能确认数据传输成功。

1.2.1 正常传输:


Ack 号为 1301 而不是 1201,原因在于 Ack 号的增量为传输的数据字节数。假设每次 Ack 号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分,比如只传递了80字节。因此按如下的公式确认 Ack 号:

Ack号 = Seq号 + 传递的字节数 + 1;

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。

1.2.2 传输异常时的处理


上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试重传数据。
为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。
上图演示的是数据包丢失的情况,也会有 ACK 包丢失的情况,一样会重传。
这里涉及3个概念:

  • 重传超时时间(RTO, Retransmission Time Out)
    这个值太大了会导致不必要的等待,太小会导致不必要的重传,理论上最好是网络 RTT 时间,但又受制于网络距离与瞬态时延变化,所以实际上使用自适应的动态算法(例如 Jacobson 算法和 Karn 算法等)来确定超时时间。
  • 往返时间(RTT,Round-Trip Time)
  • 表示从发送端发送数据开始,到发送端收到来自接收端的 ACK 确认包(接收端收到数据后便立即确认),总共经历的时延。
  • 重传次数
  • TCP数据包重传次数根据系统设置的不同而有所区别。有些系统,一个数据包只会被重传3次,如果重传3次后还未收到该数据包的 ACK 确认,就不再尝试重传。但有些要求很高的业务系统,会不断地重传丢失的数据包,以尽最大可能保证业务数据的正常交互。

最后需要说明的是,发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据。

1.3 四次握手断开


建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:
(1) 客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。
(2) 服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。
注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。
(3) 客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。
(4) 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。
(5) 客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。
(6) 服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

1.4 关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?
TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。
客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。那么,要等待多久呢?
数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。
大白话:服务器给客户端发的第三次,要是服务器收不到第四次应答,那么服务器会重发,我客户端发了第四次,要是你服务器没有重发第三次,那说明你收到了,要是重发了,那说明你没收到我发的第四次,那我再发,直到你不重发为止。

四次握手:
客户端:那我走?
服务器:等会儿,我给你拿点东西。
服务器:给你,走吧。
客户端:我真走了~
。。。
。。。
客户端:还真就不挽回,爷走了。

1.5 建议shutdown和close一起用而不是只用close

close()不管缓冲区是否还有数据,直接发送FIN包将连接关闭,并且将fd清除,程序将无法再调用数据收发函数,也没机会处理缓冲区的内容;
shutdown()只会关闭连接,而不会清除fd,在shutdown某一连接后依然可以访问另一连接,而且,shutdown会把缓冲区内容处理完之后再发送FIN包关闭连接;
所以,建议先用shutdown再close,保证缓冲区内容的处理完;

TCP 代码参考:
【Linux】网络篇二–TCP编程
如何让服务器端持续不断地监听客户端的请求?

2 UDP模型

User Datagram Protocol,用户数据报协议
UDP模型,只需要服务器创建一个socket fd,再绑上IP地址与端口号,客户端建立一个socket fd,就可以通过sendto/recvfrom函数进行通信了;sendto/recvfrom这两个函数自带地址,只需要fd就可以通信;
那么就有聪明的小伙伴要问了,服务器绑了地址,客户端不绑,那我客户端收你服务器,我能收到地址,那你服务器收我客户端,你怎么知道我的地址;你要给我发,你往哪发?
答案是,先让服务器recvfrom,就能得到客户端的地址信息;
并且,UDP中最好给服务器绑定INADDR_ANY,也就是地址0.0.0.0;表示,只要端口对的上,IP地址随意;

sendto/recvfrom
ssize_t sendto(int socket, void *message, size_t length, int flags, struct sockaddr *dest_addr, socklen_t dest_len);
ssize_t recvfrom(int socket, void *buffer, size_t length, int flags, struct sockaddr *address, socklen_t *address_len);
  • socket 由socket() 调用返回
  • message/buffer :接收/发送的信息
  • length :信息长度
  • flag :一般填0
  • struct sockaddr :存放服务器或客机的端口号与ip地址结构体
  • dest_len:结构体的大小
  • address_len :结构体的长度返回的地址

关于UDP编程代码参考:
【Linux】网络篇三–UDP编程
基于UDP的服务器端和客户端

四 IO多路复用

三个函数:select、poll、epoll;

1 select()

以下说的fd集是一个变量,按二进制值来使用,1024位;
首先,定义一个fd集,把要监测的fd加入到fd集中,调用此函数进行监测,此函数会阻塞进程,直到被监测的fd发生了状态变化,从函数返回,我们可以判断是哪个fd,进而做出相应的操作,从而实现IO的不阻塞;(阻塞下的不阻塞);

(1) fd_set是fd集类型,用它定义的变量为fd集,可以用宏来操作fd集;

FD_CLR(int fd,fd_set* set):用来清除文件描述符集合set中相关fd的位
FD_ISSET(int fd,fd_set *set):用来测试文件描述符集合set中相关fd的位是否为真
FD_SET(int fd,fd_set*set):用来设置文件描述符集合set中相关fd的位
FD_ZERO(fd_set *set):用来清除文件描述符集合set的全部位

(2) timeval是时间结构体,可以保存一个时间长度;

struct timeval
{long tv_sec; // seconds秒long tv_usec; // microseconds毫秒
}

(3) 调用select()

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

返回0表示超时了;
返回-1,表示出错了;
返回一个大于0的数,表示文件描述符状态改变的个数;

  • maxfd:需要监视的最大的文件描述符值+1;
  • readset:需要检测的可读文件描述符的集合;
  • writeset:需要检测的可写文件描述符的集合
  • exceptset:需要检测的异常文件描述符的集合
  • timeout:超时时间;超时时间有三种情况:
    • NULL:永远等待下去,仅在有一个描述字准备好I/O时才返回;
    • 0:立即返回,仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生;
    • 特定的时间值: 如果在指定的时间段里没有事件发生,select将超时返回;

调用select()会使进程阻塞,直到fd集中的一个或多个fd发生了读写或异常的状态变化,函数返回,将状态变化的fd的个数返回,并把fd集中这些fd对应的位进行置1,其余位全部清0;我们用宏来判断哪个fd是1;就知道了哪个fd可以读写了,就做出相应的操作;
由于每次select返回都会改变fd集的值,所以,在每次调用select()前,要重置fd集的值;

2 poll()

Poll的处理机制与Select类似,只是Poll选择了pollfd结构体来处理文件描述符的相关操作:

struct pollfd
{int fd;         /* 文件描述符 */short events;   /* 等待的事件 */short revents;  /* 实际发生了的事件 */
} ;
  • 每一个pollfd结构体都指定了一个文件描述符fd;
  • events代表了需要监听该文件描述的事件掩码,可选的有:
    POLLIN:有数据可读。
    POLLRDNORM:有普通数据可读。
    POLLRDBAND:有优先数据可读。
    POLLPRI:有紧迫数据可读。
    POLLOUT:写数据不会导致阻塞。
    POLLWRNORM:写普通数据不会导致阻塞。
    POLLWRBAND:写优先数据不会导致阻塞。
    POLLMSGSIGPOLL:消息可用。
  • revents代表文件描述符的操作结果掩码,内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回,除此之外,revents域还可以包含以下事件:
    POLLER:指定的文件描述符发生错误。
    POLLHUP:指定的文件描述符挂起事件。
    POLLNVAL:指定的文件描述符非法。
#include <poll.h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
  • fds:需要被监视的文件描述符集合;可以是一个结构体地址,也可以是一个结构体数组,这样就可以监测多个fd;
  • nfds:被监视的文件描述符数量;
  • timeout:超时时间,有三种取值:
    • 负数:无限超时,一直等到一个指定事件发生;
    • 0:立即返回,并列出准备好的文件描述符;
    • 正数:等待指定的时间,单位为毫秒;

poll函数与select函数的最大不同之处在于:select函数有最大文件描述符的限制,一般1024个,而poll函数对文件描述符的数量没有限制。但select和poll函数都是通过轮询的方式来查询某个文件描述符状态是否发生了变化,并且需要将整个文件描述符集合在用户空间和内核空间之间来回拷贝,这样随着文件描述符的数量增加,相应的开销也随之增加。

3 epoll()

epoll是在Linux内核2.6引进的,是select和poll函数的增强版。与select相比,epoll没有文件描述符数量的限制。epoll使用一个文件描述符管理多个文件描述符,将用户关心的文件描述符事件存放到内核的一个事件列表中,这样在用户空间和内核空间只需拷贝一次。
epoll操作是包含有三个接口的:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1) epoll_create()函数:
用创建一个epoll的句柄;size用来告诉内核这个监听的数目一共有多大,占用一个fd值;
(2) epoll_ctl()函数: epoll的事件注册函数;

  • epfd:epoll_create()的返回值;
  • op:动作,有三种取值:
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;
  • fd:需要监听的fd;
  • event: 告诉内核需要监听什么事件,
struct epoll_event
{__uint32_t events;  /* Epoll events */epoll_data_t data;  /* User data variable */
};

events取值有:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列;

**(3) epoll_wait()函数:**等待事件的产生;
参数:
events:从内核得到事件的集合,就是数组啦要先定义一个数组,然后把地址传到这来接收;
maxevents :最多返回这么多事件;
timeout:超时时间,0会立即返回,-1表示永久阻塞,正数表示一个指定的值;

工作模式:
epoll对文件描述符的操作由两种模式:水平触发LT(level trigger)和边沿触发ET(edge trigger)。默认的情况下为LT模式。LT模式与ET模式的区别在于:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

epoll相比于select/poll的优势:
从上面对select/poll/epoll函数的介绍,可以知道epoll与select/poll相比,具有如下优势:
监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目;
I/O效率不会随着监视fd的数量增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。

五 设置socket属性

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
//函数用于获得某个套接字的属性
int setsockopt(intsockfd, int level, int optname, const void *optval, socklen_t optlen);
//设置某个套接字的属性。

成功执行时,返回0。失败时,返回-1,并设置errno。

  • sockfd,就是一个fd;
  • level和optname在下面的图中;
  • optval,按下面图中数据类型项,比如int型一般是允许或者禁止,那么就定义一个"bool型"的变量,然后带入;
  • optlen就是optval的大小,一般用sizeof来算;


    请看:linux网络程序设计——3 socket属性设置

六 广播与组播

1 广播

广播就是同时发送给局域网内的所有主机,只有UDP某些可以广播;

1.1 广播地址:

(1) 每个网段最大地址:
以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址
发到该地址的数据包被所有的主机接收
(2) 255.255.255.255在所有网段中都代表广播地址

1.2 广播编程步骤:

发送方:

1、创建用户数据报套接字
2、缺省创建的套接字不允许广播数据包,需要设置属性
3、setsockopt可以设置套接字属性
4、接收方地址指定为广播地址
5、指定端口信息
6、发送数据包

socket默认属性不允许广播,要进行广播发送需设定属性

int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));

广播的接收方:

1、创建用户数据报套接字
2、绑定本机IP地址和端口,绑定的端口必须和发送方指定的端口相同
3、等待接收数据

2 组播

广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。
组播(又称为多播)是一种折中的方式。只有加入某个多播组的主机才能收到数据。
多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)

2.1 网络地址分类

  • A类地址
    第1字节为网络地址,其他3个字节为主机地址。第1字节的最高位固定为0
    1.0.0.1 – 126.255.255.255
  • B类地址
    第1字节和第2字节是网络地址,其他2个字节是主机地址。第1字节的前两位固定为10
    128.0.0.1 – 191.255.255.255
  • C类地址
    前3个字节是网络地址,最后1个字节是主机地址。第1字节的前3位固定为110
    192.0.0.1 – 223.255.255.255
  • D类地址(组播地址)
    不分网络地址和主机地址,第1字节的前4位固定为1110
    224.0.0.1 – 239.255.255.255

2.2 组播编程步骤

组播发送:

1、创建用户数据报套接字
2、接收方地址指定为组播地址
3、指定端口信息
4、发送数据包

组播接收:

1、创建用户数据报套接字
2、加入多播组(重要)
3、绑定本机IP地址和端口,绑定的端口必须和发送方指定的端口相同
4、等待接收数据

设置组播地址的结构体:

struct ip_mreq
{struct  in_addr  imr_multiaddr;  //组播地址struct  in_addr  imr_interface;  //本机地址
};

加入多播组举例:

struct  ip_mreq  mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(“235.10.10.3”);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq,  sizeof(mreq));

七 UNIX域套接字

socket同样可以用于本地通信,创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。

socket(AF_LOCAL, SOCK_STREAM, 0)
socket(AF_LOCAL, SOCK_DGRAM, 0)

分为流式套接字和用户数据报套接字
和其他进程间通信方式相比使用方便、效率更高
常用于前后台进程通信
本地地址结构

 struct sockaddr_un        //  <sys/un.h>
{sa_family_t  sun_family;char  sun_path[108];         // 套接字文件的路径
};

填充地址结构

struct sockaddr_un myaddr;
bzero(&myaddr,  sizeof(myaddr));
myaddr.sun_family = AF_UNIX;
strcpy(myaddr.sun_path,  “/tmp/mysocket”);

内容参考来源

  • 本人的学习与编程经验;
  • C语言中文网socket编程部分
  • 修成真大佬的系列文章:【Linux】网络篇

Linux应用(四)socket网络编程相关推荐

  1. Linux中的socket网络编程

    一.socket网络编程结构 tcp socket网络编程的结构: 客户端:socket-------------------------connect--------(send/recv)----- ...

  2. Linux学习之----socket网络编程基础

    分层模型 OSI七层模型 1.物理层:主要定义物理设备标准,如网线的接口类型.光纤的接口类型.各种传输介质的传输速率等.它的主要作用是传输比特流(就是由1.0转化为电流强弱来进行传输,到达目的地后再转 ...

  3. Linux C++/Java/Web/OC Socket网络编程

    一,Linux C++ Socket网络编程 1.什么是TCP/IP.UDP? TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制 ...

  4. 多实例多进程网络编程PHP,php socket网络编程基础知识(四):多进程

    标签:status   传递   windows   返回   修改   队列   _for   响应   关联 说明 php在web编程时是不需要考虑多进程的,但整个php流程是涉及到多进程的,只不 ...

  5. 基于Linux的socket网络编程项目——游侠手机商城

    基于Linux的socket网络编程项目--游侠手机商城 一.项目说明 二.项目使用的技术 三.客户端搭建 四.服务器端搭建 一.项目说明 本项目是一个仿真手机商城类系统,基本功能: 登录界面功能:用 ...

  6. Linux 网络编程——socket 网络编程

    文章目录 一.网络基础 TCP/UDP对比 TCP/IP协议族体系 socket IP地址 IP地址转化API inet_addr() inet_aton() inet_ntoa() inet_pto ...

  7. Linux Kernel TCP/IP Stack — Socket Layer — TCP/UDP Socket 网络编程

    目录 文章目录 目录 TCP/UDP Socket 逻辑架构 创建 Socket 绑定 Socket 请求建立 Socket 连接 监听 Socket 接受请求 关闭连接 数据的发送和接收 send ...

  8. linux下C语言socket网络编程简例

    转自博文:http://blog.csdn.net/kikilizhm/article/details/7858405 在练习写网络编程时,该例给了我帮助,在写服务器时,我把while逻辑位置想法错了 ...

  9. 【Linux】socket网络编程之服务器与客户端的数据交互

    [Linux]socket网络编程之服务器与客户端的数据交互 前言参考 argc !=2是什么意思 inet pton函数 对stdin,stdout 和STDOUT_FILENO,STDIN_FIL ...

最新文章

  1. VTK:绘图之AreaPlot
  2. java并发中的延迟初始化
  3. Windows Azure移动终端云服务管理(公测版)
  4. 小心错误使用EasyUI 让网站性能减半
  5. cnn加工是什么意思_天秤座R-CNN:全面平衡的目标检测器
  6. 万创帮逆向解析,让你也能体验技术变现【Python爬虫实战系列之万创帮闲置资源整合逆向】
  7. scala function从入门到理解
  8. 力扣-811 子域名访问计数
  9. LINQToSQL中如何更好的手动设置导航字段,并返回实名类型而不是匿名类型
  10. SOTA级发丝抠图模型PP-Matting重磅开源,支持多场景精细化分割!
  11. 软件工程实验报告一模板
  12. Unity中解析Excel表格工具
  13. SEO黑帽寄生虫快排的生成原理及做法[详解]
  14. eclipse工具栏全部隐藏
  15. Mr. Tsogt Batbayar 担任BCF理事
  16. 闽南歌歌词有一句电子计算机,丁丁丁丁是什么歌_抖音闽南歌曲丁丁丁丁歌名、歌手、歌词介绍_游戏吧...
  17. read和write阻塞和非阻塞方面的理解
  18. android.os.FileUriExposedException: file:///storage/emulated/0/market/cache/com.moji.mjweather.apk
  19. MVC医院信息管理系统源码 BS架构
  20. Horizontally Visible Segments

热门文章

  1. python高手之路python处理excel文件
  2. 2017计算机二c语言题库,2017计算机二c语言题库试题及答案
  3. 进出口(海关)新系统的操作流程--20040923
  4. 计组 | 周期大杂烩 —— 指令周期、时钟周期(节拍周期)、机器周期(CPU周期)等
  5. cs6导入库闪退 flash_flash cs6导入某些mp3不能的解决办法
  6. PHPMyWind编辑器支持pdf自动上传
  7. TA 数学函数与美术的结合
  8. Java智能五子棋(更新中)
  9. AWS CloudFront分发
  10. 导入Excel文件的各种常见方法