套接字是计算机进程用来交换数据的通信点,它允许两个进程,通过标准 Unix 文件描述符,进行通信,是应用层对传输层的编程接口,位于应用层和传输层之间,通过端口(port)识别进程,通过IP地址识别主机。
![e637c4a5fd71c7c4f243230d2357b154.png](https://i-blog.csdnimg.cn/blog_migrate/0f7f22a83b4d9156cde92c93a5dbfa8e.jpeg)
在 Unix 系统中,所有的 I/O 行为,都是通过读写文件描述符(一个与打开文件关联起来的整数,如网络连接、文本文件、终端等)完成。用于客户端与服务器之间的通信。
![a9c7f79342ec91f90eeaded00029e7ea.png](https://i-blog.csdnimg.cn/blog_migrate/d7630f3c9c53c66c3039aecc22b4829f.jpeg)
共有四种类型的 Socket,前两种常用。通常假定进程是在同一类型的套接字之间通信,但不同类型套接字之间亦可通信的限制。
- Stream Sockets − 使用 TCP 协议传输数据,保证收发顺序一致,如发送 "A, B, C",接收端必然会以 "A, B, C" 的顺序接收。如果发送失败,发送端会收到一个错误提示。数据包之间没有边界,需要应用层自己处理。
- Datagram Sockets − 使用 UDP 协议传输数据,通信的两个进程在发送数据前不需要建立起连接,只需要知道目的进程便可创建包并发送数据。
- Raw Sockets − 原始套接字,用于访问底层的支持“套接字抽象”的通信协议,通常是面向数据报的。用于接收本机网卡上的数据帧或者数据包、监听网络的流量和分析、开发新的通信协议。
- Sequenced Packet Sockets − 有序数据包套接字,与 stream socket 相似-具有包边界。它是 Network Systems (NS) 套接字抽象的一部分。允许用户通过它操作与接收包中 Sequence Packet Protocol (SPP) 或 Internet Datagram Protocol (IDP) 的头部。!!
网络地址-IP (host) Address
IP (host) Address 用来识别主机,IP 即 Internet Protocol ,是指因特网的整体网络架构的网络层(Internet Layer), IP = network . host ,共32 bit 或是 64 bit 。
32 为 IP 地址分类如下:
- Class A addresses begin with 0xxx, or 1 to 126 decimal.
- Class B addresses begin with 10xx, or 128 to 191 decimal.
- Class C addresses begin with 110x, or 192 to 223 decimal.
- Class D addresses begin with 1110, or 224 to 239 decimal 多播地址
- Class E addresses begin with 1111, or 240 to 254 decimal 预留地址
Addresses beginning with 127 decimal, are reserved for loopback and for internal testing on a local machine [You can test this: you should always be able to ping 127.0.0.1, which points to yourself];
可以通过对 host 进一步划分产生子网,控制流量或是适应硬件异构,即host = subnet . subhost 。
客户端服务器模型
大多少网络应用都采用客户服务器模型。
![ab5f2bad4a0e757233ab2baccca565c1.png](https://i-blog.csdnimg.cn/blog_migrate/0005a524793947ee97feacdd676e7273.jpeg)
2-tier 与 3-tier 架构
There are two types of client-server architectures −
- 2-tier 架构 − 客户端直接与服务端通信,该模型通常会有安全漏洞与性能问题,可以通过 Secure Socket Layer (SSL)解决安全问题。
- 3-tier 架构 − 客户端与服务端直接,会插入一个或是多个软件-中间件,用来进行安全检验,做负载均衡。
![961d5b0a286936b3a964189fb5e369b1.png](https://i-blog.csdnimg.cn/blog_migrate/5faf532b42e8ff21d60f308a580a9898.jpeg)
Stream Sockets-创建 socket 步骤
客户端创建 socket 步骤:
- Create socket()
- Connect connect()
- Send & receive data. 有多种实现方式,最简单是使用 read() and write()
服务端创建 socket 步骤:
- Create socket()
- Bind socket to address bind() 因特网地址 = port number + host
- Listen connections listen()
- Accept connection accept() ,blocks connection until a client connects with the server
- Send and receive data with read() and write()
![e9c64b444810308a7e18232235a2bf7a.png](https://i-blog.csdnimg.cn/blog_migrate/144c03152fe9858e37156262bbf5246e.png)
Socket 结构体
struct sockaddr {
unsigned short sa_family;
char sa_data[14];//sockaddr_in
};
sa_family,地址家族,通常使用 AF_INET,{ AF_INET、AF_UNIX、AF_NS、AF_IMPLINK }
sa_data,根据 sa_family 解析,对于 AF_INET 协议家族,port number 和 IP address 存储在其中,即 sockaddr_in 结构体
struct sockaddr_in {
short int sin_family; //同sa_family
unsigned short int sin_port; //服务端口,16位的端口号,Network Byte Order
struct in_addr sin_addr; //IP地址,32位IP地址,Network Byte Order
unsigned char sin_zero[8];//NULL 没有使用
};
struct in_addr {
unsigned long s_addr;//32 bit netid/hostid Network Byte Order
};
主机信息结构
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list
#define h_addr h_addr_list[0]
};
服务端信息结构
struct servent {
char *s_name;
char **s_aliases;
int s_port;
char *s_proto;
};
结构使用原则
- 使用引用传递结构,用另外一个参数传递结构体的长度。
- 如果一个函数填充结构体,使用引用传递结构体长度,即所谓的 value-result 参数。
- 使用bzero() 前必须使用 memset() 把结构体设置为 NULL。
端口与服务
使用 32-bit IP 地址识别机器,TCP 和 UDP 使用知名的端口(port)来识别进程。
客户端连接到服务端之前,必须有识别服务端的方法才可以。知道了IP地址就可以连接服务端了,但是如何识别某个进程呢?通信是进程间的通信,只识别主机进程是无法进行通信的。
![2d0116c7b6c40c8d5973bd0fd288de75.png](https://i-blog.csdnimg.cn/blog_migrate/0aa7741f693117ad893cef5101c0b75c.jpeg)
为了解决识别通信的进程,TCP 和 UDP 定义了一组公认的端口(ports),即所有小于1024的端口,自定义端口号的阈为 [1024, 65535]。各种进程的端口号配置可在 /etc/services 查询。
If you are writing your own server then care must be taken to assign a port to your server. You should make sure that this port should not be assigned to any other server. 用尽了咋办?Normally it is a practice to assign any port number more than 5000.
端口分配与关联的服务:https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
端口与服务函数
Unix 提供的从/etc/services 获取服务名和端口号的函数:
- struct servent *getservbyname(char *name, char *proto) − map service name and protocol name to port number
- struct servent *getservbyport(int port, char *proto) − map port number and protocol name to service name.
Socket 网络字节序(与大端一致)
不同的计算机,可能以不同的顺序,存储多字节值。 如一个两个字节,即16比特的因特网,由两种方式存储其值:
- Little Endian 小端 − 把表意能力差的字节,放在低地址(地址A),表意能力强的字节,方在高地址(地址A+1)。
- Big Endian 大端 - 与小端相反。
在建立因特网 socket 连接的过程中,必须保证结构体 sockaddr_in 的成员 sin_port 和 sin_addr ,以网络字节序表象。
下面是进程主机内部的数据表象形式和网络字节序数据表象形式转换历程:
- unsigned short htons(unsigned short host short) − (2-byte) host byte order to network byte order.
- unsigned long htonl(unsigned long host long) − (4-byte) quantities from host byte order to network byte order.
- unsigned short ntohs(unsigned short net short) − 16-bit (2-byte) quantities from network byte order to host byte order.
- unsigned long ntohl(unsigned long net long) − 32-bit quantities from network byte order to host byte order.
Socket - IP 地址函数
Unix 提供多种操作 IP 地址的函数,即 IP 地址函数,把因特网地址在 ASCII 字符串(人类易读)和网络字节序的二进制值(存储在 socket 地址结构中的值)之间相互转换。
IPv4 addressing
int inet_aton(const char *strptr, struct in_addr *addrptr)
It returns 1 if the string was valid and 0 on error.
#include <arpa/inet.h>
(...)
int retval;
struct in_addr addrptr //Network Byte Order (bytes ordered from left to right)
memset(&addrptr, '0', sizeof(addrptr));
retval = inet_aton("68.178.157.132", &addrptr);
(...)
in_addr_t inet_addr(const char *strptr)
This function call converts the specified string in the Internet standard dot notation to an integer value suitable for use as an Internet address. The converted address will be in Network Byte Order (bytes ordered from left to right). It returns a 32-bit binary network byte ordered IPv4 address and INADDR_NONE on error.
Following is the usage example −
#include <arpa/inet.h>
(...)
struct sockaddr_in dest;
memset(&dest, '0', sizeof(dest));
dest.sin_addr.s_addr = inet_addr("68.178.157.132");
(...)
char *inet_ntoa(struct in_addr inaddr)
This function call converts the specified Internet host address to a string in the Internet standard dot notation.
Following is the usage example −
#include <arpa/inet.h>
(...)
char *ip;
ip = inet_ntoa(dest.sin_addr);
printf("IP Address is: %sn",ip);
(...)
Socket 核心流程
![e9c64b444810308a7e18232235a2bf7a.png](https://i-blog.csdnimg.cn/blog_migrate/144c03152fe9858e37156262bbf5246e.png)
- int socket (int family, int type, int protocol); 双端创建socket
- int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 客户端发起连接
- int bind(int sockfd, struct sockaddr *my_addr,int addrlen); 服务端绑定协议地址(IP + port)
- int listen(int sockfd,int backlog); 服务端,把未连接的 socket 设置为被动 socket,意味着内核可以把新来的请求并交给它处理;内核入队的该 socket 的最大连接数为 backlog;
- int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); 返回连接队列中的下成功连接的文件描述符
高级概念
socket 有三种请求-服务模型:多线程、select() 和 poll():
使用多线程,一个请求一个线程为其服务。
select() 使用一个 bit 数组,来标识从 0 到 n-1 连续的 n个描述符是否需要读写。
poll() 使用离散的 pollfd 集合来标识一个个的描述符,不像 bit 数组,这里是按需分配 pollfd 结构体,不会出现浪费。
资源消耗的复杂行---逻辑操作的复杂行---面向对象的封装 poll
异步 I/O(overlapped I/O)
基于线程的客户端/服务器模型,可以使用异步 I/O 完成高并发且高效内存使用的 I/O。
- 一个客户端连接一个线程,会消耗太多线程,引入大量的 sleep 和 wake-up 消耗。
- 使用 select() ,多个客户端连接复用一个线程,弊端是引入了大量的冗余工作-选择或是标识每一个后续的 select ,逻辑复杂化。
在控制权交给用户应用后,异步 I/O 通过用户空间 buffers 的写入或读出,克服了上面两种方案的缺点。在数据可读时,或是连接可写入数据时,异步 I/O 通知这些工作线程。
阻塞 I/O
在阻塞 I/O 中,客户端到服务端的一个连接请求,会占用一个服务端线程。直到读线程读取完整的数据,放入缓冲中后才通知工作线程开始处理数据,在这个很长的等待期内,读线程空闲。select/poll 也是是阻塞 I/O 。
非阻塞 I/O
异步非阻塞 I/O 快速但是消耗CPU,是真正额异步 I/O 。
Select/poll 是异步阻塞的,扩展性差。
使用 fcntl() 打开 O_NONBLOCK ,或是使用 ioctl() 打开 FIONBIO ,立即返回不阻塞(默认阻塞),如果没有达到目的可以使用 poll() 或是 select()。
cntl是计算机中的一种函数,通过fcntl可以改变已打开的文件性质。
信号
应用可以向系统注册事件回调,事件发生后,使用 poll() 或是 select() 查看为什么接收到了信号。
IP 多播
应用发送一个 IP 数据报,网络中的一个组内的多个主机可以收到该 IP 数据,主机可在不同的网络中,可以随时向组内添加或是移除主机。
带外数据 Out-of-band Data
带内数据是正常的有序流数据,带外数据独立于带内数据。带外数据是一个概念上独立通道,其数据传输不需要保持顺序,是传输协议和数据通道固有的特性。该概念可能来源于通信行业的带外信号,即 out-of-band signaling 。
I/O 多播 - select()
select() 用于检哪个文件描述符可读、可写或是发生了错误。建议使用异步 I/O ,避免 select() ,以最大限度的使用应用的资源。
同异步 I/O ,select() 创建一个共用点,同时等待多个条件,它允许应用指定一组描述符,监测一下条件是否存在:
- 有数据可读
- 可写数据
- 发生异常
![fa75eae7fba5198c758467331240cc27.png](https://i-blog.csdnimg.cn/blog_migrate/37724e54a8237464969c9eaf907cf484.jpeg)
小结
一共有五种 I/O 模型,即默认的 Blocking 模型、通过 fcntl() 设置 O_NONBLOCK 的 Nonblocking 模型、使用 select() 或 poll() 的 I/O multiplexing 模型、信号驱动的 I/O 模型、通过 ioctl() 设置 FIOASYNC = O_ASYNC 的 Asynchronous I/O 模型。
https://www.tutorialspoint.com/unix_sockets/index.htm
https://www.binarytides.com/multiple-socket-connections-fdset-select-linux/
https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzab6/coobd.htm
https://luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking
http://www.differencebetween.net/language/difference-between-synchronous-and-asynchronous/