套接字的使用,以及使用过程中的注意事项和相关知识
套接字socket
socket
套接字本来是用于同主机的进程间通信的。后来有了TCP/IP协议族
加入后又进行的功能的拓展与升级,实现了多个不同主机间的进程间通信。
Linux内核的五大功能:
- 进程间管理:进程间调度及上下文切换
- 内存管理:内存的分配与回收。
- 文件管理:将二进制数据进行长时间的保存及管理。
- 设备管理:一切皆文件。
- 网络管理:网络协议栈的管理。
socket
是一个系统调用的接口,会返回一个文件描述符,用户通过网络收发数据时,只需对此文件描述符进行读写操作即可,读就是接收,写就是发送。相当于把复杂的网络通信过程转换成了IO操作。
也可以将Socket
(套接字)看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。
套接字Socket
=(IP地址:端口号),套接字的表示方法是点分十进制的lP
地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)
套接字类型
**流套接字(SOCK_STREAM)**:流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议
**数据报套接字(SOCK_DGRAM)**:数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理
**原始套接字(SOCK_RAW)**:原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接
字节序
字节序是计算机存储多字节数据的方式,目前主流的方式有:大端字节序和小端字节序,字节序主要是针对多字节的数据类型,比如 short、int 等类型
- 小端字节序
数据高位字节存储在内存的高地址上,数据低位字节存储在内存的低地址上 - 大端字节序
数据高位字节存储在内存的低地址上,数据低位字节存储在内存的高位地址上
因为大端对人们阅读更友好,所以 IEEE 标准协会规定除非有明确说明,否则网络协议都使用大端字节序, 像 TCP/IP 就使用大端序。而计算机的存储则以小端序为主。
判断大小端
1 |
|
大小端转换
1 | //如果是Linux: |
1 |
|
IP地址转换
1 | // 将点分十进制字符串转换成网络字节序的无符号四字节整形的转换函数: |
1 |
|
端口号
网络通信中的两个进程间互联时的一种约定好的进程的标识
端口号使用规则 :TCP
与UDP
段结构中端口地址都是16比特,可以有在0—65535范围内的端口号。对于这65536个端口号有以下的使用规定
- 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何
TCP/IP
实现所提供的服务都用1—1023之间的端口号,是由ICANN
来管理的;端口号从1024—49151是可被注册的端口,也成为“用户端口”,49152-65535被IANA
指定为特殊服务使用。在Linux
中可以通过/etc/services
下可查看已经占用的端口号。 - 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号。
- 大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。
端口号是2^16
次方,所以是超过了一个字节,是两个字节,所以在指定端口时,也需要注意向网络字节序进行转换的问题。
所以字节或字节数组在网络传输不需要关系字节序的问题,但是对于多字节的数据类型一定要注意字节序的问题。
Client/Server(C/S)
- 创建套接字
- 套接字绑定
- 监听套接字
- 建立链接请求
- 读写
- 关闭套接字
创建套接字 - socket
1 |
|
内核中的事情:
当进程调用socket()函数创建一个套接字时,操作系统会在内核中分配一些资源来管理这个套接字。具体来说,内核会创建一个数据结构来表示该套接字,该数据结构存储了套接字的各种属性,如协议类型、地址信息、发送和接收缓冲区等。此外,内核还会分配一些缓存区来存储套接字的数据,例如接收到的数据和将要发送的数据。
当进程调用bind()函数绑定套接字地址时,内核会将指定的套接字地址绑定到套接字数据结构中,以便进程可以通过该套接字地址来发送和接收数据。如果进程调用listen()函数将套接字转换为被动套接字,内核还会为套接字创建一个等待连接的队列。
总之,socket()函数会在内核中开辟一些资源来管理套接字,包括套接字的数据结构和缓存区等。这些资源会随着套接字的使用而不断变化,内核会负责管理这些资源,以保证进程可以正常地发送和接收数据。
套接字绑定 - bind
1 |
|
内核中的事情:
- 内核会检查传递的网络信息结构是否合法。如果结构体不合法,bind()函数会失败并返回一个错误。
- 如果结构体合法,则内核会将套接字与指定的网络地址和端口号绑定。这将为套接字分配一个本地 IP 地址和端口号,并将该地址添加到套接字的控制块中。
- 如果指定的端口号为0,则内核会为套接字分配一个可用的随机的端口号。
- 如果套接字绑定的是 INADDR_ANY(通配地址),则内核会将套接字绑定到所有可用的网络接口,并为套接字分配一个本地 IP 地址。
- 内核会根据套接字的类型和协议确定套接字的传输方式,并分配一些内部数据结构和缓冲区来管理套接字。
总之,通过将套接字的网络信息结构传递给bind()函数,内核会将套接字与指定的网络地址和端口号绑定,并执行一系列操作来为套接字分配地址、管理套接字并提供网络通信服务。
监听套接字 - listen
1 |
|
内核中的事情:
- 内核会将该套接字标记为被动套接字(passive socket),即告诉内核该套接字将被用来接受客户端连接请求。
- 内核会创建一个等待连接的队列,并将该套接字加入到该队列中。客户端连接请求首先会被放入等待连接的队列中,然后再由进程调用accept()函数来处理它们。
- 内核会开始监听该套接字,以便接受来自客户端的连接请求。这意味着内核会开始接受传入的连接请求,但并不会自动建立连接,而是将连接请求放入等待连接的队列中。
listen()
函数会返回一个成功的状态,表示该套接字已经被设置为监听状态。
总之,listen()函数背后内核会执行一系列的操作来将一个套接字设置为监听状态,包括创建等待连接的队列、加入套接字到等待连接队列、开始监听套接字等。通过这些操作,内核为进程提供了一种简单、可靠的方式来接受来自客户端的连接请求。
建立链接请求 - accept
1 |
|
内核中的事情:
- 当调用accept()函数时,内核会检查该套接字是否处于监听状态。如果不是,会阻塞等待直到该套接字处于监听状态。
- 内核会检查等待连接的队列中是否有客户端连接请求,如果有,则内核会创建一个新的套接字来处理该客户端连接,并从等待连接的队列中删除该连接请求。如果没有,则accept()函数会阻塞等待,直到有连接请求到达为止。
- 内核会为新创建的套接字分配一个新的文件描述符,并把它返回给进程。这个新的文件描述符和原来的套接字文件描述符是不同的。
- 接下来,内核会为新创建的套接字建立一个连接,并返回一个连接成功的状态。
- 如果连接建立成功,则进程就可以使用新的套接字来与客户端进行通信了。如果连接建立失败,则会返回一个错误码,进程需要根据错误码进行处理。
总之,accept()
函数背后内核会执行一系列的操作来处理客户端连接请求,包括检查监听状态、等待连接请求、创建新的套接字、分配新的文件描述符、建立连接等。通过这些操作,内核为进程提供了一种简单、可靠的方式来处理客户端连接请求。
通信的本质 - read()/write() — 数据收发
1 |
|
内核中的事情:
在调用read()函数读取套接字时
- 内核会检查套接字的接收缓冲区是否有数据可读。如果接收缓冲区为空,则
read()
函数会阻塞等待,直到有数据可读。 - 如果接收缓冲区有数据可读,则内核会将数据从接收缓冲区复制到进程的缓冲区中。
- 内核会检查进程的缓冲区是否有足够的空间来存储读取的数据。如果没有足够的空间,则
read()
函数会阻塞等待,直到有足够的空间为止。 - 如果有足够的空间,则内核会将读取的数据从内核空间复制到进程的缓冲区中,并更新套接字的接收缓冲区的读指针。读指针指向下一个要读取的位置。
- r
ead()
函数会返回读取的字节数,如果读取到套接字关闭,则返回0。
总之,调用read()
函数读取套接字时,内核会执行一系列的操作来读取套接字中的数据,包括检查接收缓冲区、阻塞等待、读取数据、检查缓冲区空间、复制数据到进程空间等。通过这些操作,内核为进程提供了一种简单、可靠的方式来读取套接字中的数据。
在调用write()函数写入套接字时
- 内核会将写入的数据从进程的缓冲区复制到套接字的发送缓冲区中。
- 内核会检查套接字的发送缓冲区是否有足够的空间来存储写入的数据。如果发送缓冲区已满,则
write()
函数会阻塞等待,直到有足够的空间为止。 - 如果发送缓冲区有足够的空间,则内核会将数据从进程空间复制到套接字的发送缓冲区中,并更新发送缓冲区的写指针。写指针指向下一个要写入的位置。
write()
函数会返回写入的字节数。
总之,调用write()
函数写入套接字时,内核会执行一系列的操作来写入套接字中的数据,包括复制数据到发送缓冲区、检查缓冲区空间、更新写指针等。通过这些操作,内核为进程提供了一种简单、可靠的方式来写入套接字中的数据。
关闭套接字 - close
1 |
|
内核中的事情:
当进程调用close()
函数关闭套接字时,内核会释放该套接字占用的所有资源,包括套接字的数据结构、缓存区、等待连接的队列等。此时,进程不再能够使用这个套接字。
另外,如果进程崩溃或者异常终止,内核会自动回收该进程占用的所有资源,包括该进程创建的所有套接字。这个过程称为垃圾回收(Garbage Collection),由操作系统内核自动完成。
注意:如果有其他进程仍在使用某个套接字,那么该套接字的资源就不会被释放,直到所有使用该套接字的进程都调用了
close()
函数关闭它。这是因为操作系统内核负责管理套接字资源,并且要确保多个进程可以共享同一个套接字。只有当所有使用该套接字的进程都退出或关闭它时,内核才会回收该套接字的所有资源。