Linux网络编程-socket
socket 套接字
图中两个虚线框表示两个套接字,在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。
一个socket只有一个文件描述符,即发送缓冲区和接收缓冲区使用同一个文件描述符。
网络套接字本质:一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)。
网络地址
字节序转换
由于历史遗留问题,网络数据流采用大端字节序,而 pc 本地一般采用的小段字节序,所以在网络通信中,需要转换字节序。
- 小端法:(pc本地存储)高位存高地址,地位存低地址。
- 大端法:(网络存储)高位存低地址,地位存高地址。
字节序只影响不同字节间的顺序,如果只看单字节内部,大小端都一样。不存在00001111
变为11110000
!
1 |
|
inet_pton 函数
将一个点分十进制的 IP
地址(字符串)转换为一个32位的整数。
1 |
|
- af:地址族,可以是
AF_INET(ipv4)
、AF_INET6(ipv6)
- src:要转换的
IP
地址,如:"192.168.1.1"
- dst:转换后的网络字节序的
IP
地址,整数,如:&ipv4_addr
- 返回值:
- 1,成功
- 0,异常,说明
src
不是一个合法的ip地址 - -1,
af
不是一个合法的地址族
inet_ntop 函数
将一个网络字节序的IP地址转换为一个点分十进制的IP地址(字符串)。
1 |
|
- af:地址族,可以是
AF_INET(ipv4)
、AF_INET6(ipv6)
- scr:网络字节序的IP地址
- dst:转换后的点分十进制的IP地址,缓冲区,
char buf[16]
- size:dst 的大小
- 返回值:成功返回 dst, 失败返回 NULL
inet_addr 函数
将一个点分十进制的IP地址(字符串)转换为一个32位的整数。
1 |
|
- cp:要转换的IP地址,如:
192.168.10.1
- 返回值:成功返回网络字节序的IP地址,失败返回
INADDR_NONE(-1, 0xffffffff)
使用这个函数可能会出现问题,因为 INADDR_NONE
也是一个有效的IP地址255.255.255.255
!
sockaddr 结构
同样是历史遗留问题,socket 相关的函数大多都是使用 struct sockaddr
结构,但现在使用的往往是 struct sockaddr_in
,因此在传递参数时,要进行强制类型转换。
可在 man 7 ip
中查看相关说明
1 | struct sockaddr { |
socket 创建流程
服务器端:
socket()
创建一个 socket(socket1)bind()
绑定 IP + port(设置 socket1)listen()
设置同时监听上限(socket1用于监听)accept()
阻塞监听客户端连接(传入socket1以建立连接,创建socket2用于通信)read()/write()
文件处理的系统调用函数,这里就直接像访问正常文件一样访问socket2
客户端:
socket()
创建一个 socket(socket3)connect()
指定服务器的地址结构(IP+port)连接服务器(设置socket3)read()/write()
注意,在上图的流程中,最终会有3个 socket 结构,服务器端的 socket1 仅用于监听,并不负责实际的与客户端通信,accept()
函数阻塞等待客户端连接,当有客户端连接时,accept()
函数中会新建一个 socket2 与客户端 socket3 绑定,实现客户端与服务器端的通信。
socket 函数
创建一个套接字,并返回一个套接字描述符。
1 |
|
- domain:地址族,常用三个为
AF_INET(ipv4)
、AF_INET6(ipv6)
、AF_UNIX(本地套接字)
- type:套接字类型,常用两个为
SOCK_STREAM(TCP)
、SOCK_DGRAM(UDP)
- protocol:协议,一般设置为 0
- 返回值:成功返回套接字描述符,失败返回 -1,并设置 errno
bind 函数
给 socket 绑定一个地址结构(IP + port
)
1 |
|
- sockfd:套接字文件描述符
- addr:地址结构(传入参数)
- addrlen:地址结构的大小
sizeof(addr)
- 返回值:成功返回 0,失败返回 -1,并设置 errno
listen 函数
设置同时与服务器建立连接上限数(同时进行TCP三次握手的客户端数量)
1 |
|
- sockfd:套接字文件描述符
- backlog:上限数量,最大值为
128
! - 返回值:成功返回 0,失败返回 -1,并设置 errno
accept 函数
阻塞等待客户端连接,当有客户端连接时,会新建一个 socket2 与客户端 socket3 绑定,实现客户端与服务器端的通信,返回新建的 socket2 的文件描述符。
1 |
|
- sockfd:传入的套接字,上面绑定了地址的 socket1
- addr:传出参数,返回成功建立连接的客户端的地址结构
- addrlen:传入传出参数,传入为参2 addr 的大小,传出为客户端的 addr 的实际大小
- 返回值:成功返回新建的 socket2 文件描述符,失败返回 -1,并设置 errno:
- EINTR:被信号中断
- ECONNABORTED:连接被拒绝
connect 函数
客户端连接到服务器!
客户端 socket 可以不需要手动 bind,系统会自动绑定本地的地址结构!
1 |
|
- sockfd:套接字文件描述符
- addr:服务器的地址结构
- addrlen:地址结构的大小
sizeof(addr)
- 返回值:成功返囟 0,失败返囟 -1,并设置 errno
TCP example
简单的 TCP 收发测试!
1 | // 服务器端 |
1 | // 客户端 |
nc
命令也可以用来做客户端测试!
1 | nc 192.168.0.8 10001 |
read 函数
从指定的文件描述符上最多读取 count
个字节存放到缓冲区 buf
中,返回实际读取的字节数。
1 |
|
在网络编程中,read 函数的返回值需要仔细区分:
>0
实际读到的字节数;=0
已经读到结尾,说明对端socket已关闭(重点!!);-1
应该进下一步判断errno
的值:EAGAIN/ENOULDBLOCK
设置了非阻塞方式读,没有数据到达EINTR
慢速系统调用被 中断- 其他情况,异常
write 函数
将缓冲区 buf
中的 count
个字节数据写入到 fd
对应的文件中,返回实际写入的字节数。
1 |
|
recv/recvfrom 函数
从指定的 sockfd
上接收(读取)数据写入到缓冲区 buf
中,buf
的长度是len
,返回实际接收的字节数。
1 |
|
flags
参数可以用来指定该函数的特殊行为。常用的 flags
参数值有:
0
:不设置标志。MSG_PEEK
:该标志会导致recv
函数返回套接字中的数据,但不会从缓冲区中删除数据。下一次调用recv
函数会再次返回相同的数据。这个标志对检查接收到的数据而不消耗它很有用。MSG_WAITALL
:该标志导致recv
函数阻塞,直到接收到请求的字节数或连接关闭。如果在接收到请求的字节数之前连接关闭,recv
函数会返回已接收的字节数。MSG_OOB
:该标志导致recv
函数返回带外数据,如果有可用的带外数据。带外数据是单独从正常数据流发送的数据,通常用于紧急数据。MSG_DONTWAIT
:该标志等同于O_NONBLOCK
文件状态标志。当设置该标志时,recv
函数不会阻塞,如果没有数据可用,则返回-1
并带有错误代码EAGAIN
或EWOULDBLOCK
。
这些标志可以使用位运算符 |
组合,例如:
1 | int n = recv(sockfd, buffer, 1024, MSG_PEEK | MSG_WAITALL); |
send/sendto 函数
将缓冲区buf
中的len
个字节的数据发送到对应的sockfd
,返回实际发送的字节数。
1 |
|
send()
/write()
的一点区别:首先,这两者都是系统调用,都是用来向文件描述符(包括
Socket
文件描述符)写入数据的。区别在于,send()
系统调用可以指定一些可选的参数,例如flags
参数用来指定发送数据的方式,如非阻塞方式和带外方式等。send()
函数比write()
函数更加灵活,因此在网络编程中更常用。
get/setsockopt 函数
用于设置套接字的一些特殊选项!
1 |
|
socket
:表示需要设置选项的套接字的描述符。level
:表示选项的协议层,通常是SOL_SOCKET
,表示套接字选项。optname
:表示选项的名称,根据不同的协议层,选项可能不同。optval
:指向包含选项值的缓冲区的指针。optlen
:缓冲区的大小。
SO_SNDBUF/SO_RCVBUF
SO_SNDBUF
选项控制发送缓冲区的大小,即从应用程序发送到套接字的数据在内核中的缓冲区大小。如果应用程序发送的数据量大于该缓冲区大小,则必须等待先前发送的数据完成发送才能继续发送数据。
SO_RCVBUF
选项控制接收缓冲区的大小,即从网络接收到的数据在内核中的缓冲区大小。如果从网络接收的数据量大于该缓冲区大小,则新接收到的数据可能会覆盖先前未读取的数据。
通过使用 setsockopt
函数,可以修改发送缓冲区和接收缓冲区的大小,以适应应用程序的数据传输需求。但是,修改缓冲区大小并不能保证性能提升,因为实际情况可能受到许多因素的影响。
SO_LINGER
1 | struct linger { |
SO_LINGER
选项是一个套接字选项,用于控制在关闭套接字时是否等待正在发送的数据的完成。
当关闭套接字时,如果有数据正在发送,那么通常有两种处理方式:
- 如果未启用
SO_LINGER
选项,那么套接字会立即关闭,未发送完的数据会被丢弃。 - 如果启用了
SO_LINGER
选项,则套接字将等待所有正在发送的数据完成,直到指定的时间到达为止。
具体的,可以使用 setsockopt
函数启用 SO_LINGER
选项,并使用一个结构体来指定等待的时间,该结构体由两个成员:
l_onoff
:用于控制是否启用SO_LINGER
选项。l_linger
:表示等待时间,单位为秒。
如果 l_onoff
设置为非零值,则启用 SO_LINGER
选项,并使用 l_linger
指定的等待时间。如果 l_onoff
设置为零,则禁用 SO_LINGER
选项,关闭套接字时不等待数据的完成。
SO_KEEPALIVE
SO_KEEPALIVE
选项用于控制是否启用 TCP 连接的 keep-alive 检测机制。当启用该选项时,如果两端在一段时间内没有数据交互,TCP 协议会发送 keep-alive 数据包来检测对端是否仍然可用。如果多次尝试后对端仍然无法响应,则可以断开该连接。
这个选项可以用于防止长时间空闲的连接被网络中断,从而导致不必要的等待和重试。在某些情况下,它也可以用于检测对端的不存在或故障。
SO_REUSEADDR
SO_REUSEADDR
选项用于控制在同一个主机上是否允许多个套接字绑定到同一端口。
在默认情况下,如果一个套接字已经绑定到某个端口,那么其他套接字将不能再绑定到该端口。这种限制有助于防止在同一端口上的竞争冲突。
但是,有时需要多个套接字共享同一端口,例如当同一主机上有多个服务器程序运行时。在这种情况下,可以使用 SO_REUSEADDR
选项来允许多个套接字绑定到同一端口。
SO_REUSEPORT
SO_REUSEPORT
选项是一种扩展的套接字选项,主要用于允许多个套接字绑定到同一端口,从而共享同一个端口。这个选项与 SO_REUSEADDR
选项类似,但是提供了更多的灵活性和更高的性能。
在默认情况下,如果一个套接字已经绑定到某个端口,那么其他套接字将不能再绑定到该端口。但是,如果多个套接字都启用了 SO_REUSEPORT
选项,那么多个套接字就可以同时绑定到同一端口。
这个选项对于提高网络服务的性能和可靠性非常重要,因为它可以允许多个进程或线程共享同一端口,从而利用多核处理器的优势。
使用 setsockopt
函数,可以在套接字创建后启用或禁用 SO_REUSEPORT
选项。请注意,SO_REUSEPORT
选项可能只在某些操作系统上可用,并且可能需要在编译套接字应用程序时启用特定的宏定义才能使用该选项。