Linux Socket编程
Socket
- 套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过, 使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上 的不同应用程序。socket IPC 通常使用客户端<—>服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
- 当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准(BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到 任何实现 BSD socket 标准的平台,譬如 LwIP,它兼容 BSD Socket;又譬如 Windows,它也实现了一套基于 socket 的套接字接口,更甚至在国产操作系统中,如 RT-Thread,它也实现了 BSD socket 标准的 socket 接 口。
Socket编程
头文件
socket()函数
- socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回 一个网络文件描述符,通常把这个文件描述符称为 socket 描述符(socket descriptor),这个 socket 描述符跟 文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
|
参数 domain 用于指定一个通信域;这将选择将用于通信的协议族。可选的协议族如下表所示


对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可 以选择 AF_INET6。
参数 type 指定套接字的类型,当前支持的类型有
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类 型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为 SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol,TCP 协议)。在 AF_INET 通 信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
调用 socket()与调用 open()函数很类似,调用成功情况下,均会返回用于文件 I/O 的文件描述符,只不 过对于 socket()来说,其返回的文件描述符一般称为 socket 描述符。当不再需要该文件描述符时,可调用 close()函数来关闭套接字,释放相应的资源。
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字 |
bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
- bind()函数用于将一个 IP 地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)。
- 一般来讲,会将一个服务器的 套接字绑定到一个众所周知的地址—即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址 (注意这里说的地址包括 IP 地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道 服务器的 IP 地址以及对应的端口号,所以通常服务器的 IP 地址以及端口号都是众所周知的。
- 参数 addr 是一个指针,指向一个 struct sockaddr 类型变量
struct sockaddr { |
- 第二个成员 sa_data 是一个 char 类型数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口 号等信息,这个结构对用户并不友好,它把这些信息都封装在了 sa_data 数组中,这样使得用户是无法对 sa_data 数组进行赋值。事实上,这是一个通用的 socket 地址结构体。
- 一般我们在使用的时候都会使用 struct sockaddr_in 结构体,sockaddr_in 和 sockaddr 是并列的结构(占 用的空间是一样的),指向 sockaddr_in 的结构体的指针也可以指向 sockadd 的结构体,并代替它,而且 sockaddr_in 结构对用户将更加友好,在使用的时候进行类型转换就可以了。
struct sockaddr_in { |
listen()函数
- listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函 数在一般在 bind()函数之后调用,在 accept()函数之前调用
int listen(int sockfd, int backlog); |
- 参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请 求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户 过多,使得服务器进程无法快速地完成所有的连接请求.因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队 列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所 以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
服务器流程
- 调用 socket()函数打开套接字;
- 调用 bind()函数将套接字与一个端口号以及 IP 地址进行绑定;
- 调用 listen()函数让服务器进程进入监听状态,监听客户端的连接请求;
- 调用 accept()函数处理到来的连接请求。
accept()函数通常只用于服务器应用程序中,如果调用 accept()函数时,并没有客户端请求连接(等待连 接队列中也没有等待连接的请求),此时 accept()会进入阻塞状态,直到有客户端连接请求到达为止。
当有 客户端连接请求到达时,accept()函数与远程客户端之间建立连接,accept()函数返回一个新的套接字。这个 套接字与 socket()函数返回的套接字并不同,socket()函数返回的是服务器的套接字(以服务器为例)
accept()函数返回的套接字连接到调用 connect()的客户端,服务器通过该套接字与客户端进行数据交互,譬 如向客户端发送数据、或从客户端接收数据。
参数 addr 是一个传出参数,参数 addr 用来返回已连接的客户端的 IP 地址与端口号等这些信息。参数 addrlen 应设置为 addr 所指向的对象的字节长度,如果我们对客户端的 IP 地址与端口号这些信息不感兴趣, 可以把 arrd 和 addrlen 均置为空指针 NULL。
connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
- 该函数用于客户端应用程序中,客户端调用 connect()函数将套接字 sockfd 与远程服务器进行连接,参 数 addr 指定了待连接的服务器的 IP 地址以及端口号等信息,参数 addrlen 指定了 addr 指向的 struct sockaddr 对象的字节大小。
- 客户端通过 connect()函数请求与服务器建立连接,对于 TCP 连接来说,调用该函数将发生 TCP 连接的 握手过程,并最终建立一个 TCP 连接,而对于 UDP 协议来说,调用这个函数只是在 sockfd 中记录服务器 IP 地址与端口号,而不发送任何数据。
- 函数调用成功则返回 0,失败返回-1,并设置 errno 以指示错误原因。
发送和接收函数
read()函数
- 通过 read()函数从一个文件描述符中读取指定字节大小的数据并放入到指 定的缓冲区中,read()调用成功将返回读取到的字节数,此返回值受文件剩余字节数限制,当返回值小于指 定的字节数时并不意味着错误;这可能是因为当前可读取的字节数小于指定的字节数(比如已经接近文件 结尾,或者正在从管道或者终端读取数据,或者 read()函数被信号中断等),出错返回-1 并设置 errno,如果 在调 read 之前已到达文件末尾,则这次 read 返回 0。
- 套接字描述符也是文件描述符,所以使用 read()函数读取网络数据时,read()函数的参数 fd 就是对应的 套接字描述符。
recv()函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
不论是客户端还是服务器都可以通过 revc()函数读取网络数据,它与 read()函数的功能是相似的。参数 sockfd 指定套接字描述符,参数 buf 指向了一个数据接收缓冲区,参数 len 指定了读取数据的字节大小,参 数 flags 可以指定一些标志用于控制如何接收数据。
函数 recv()与 read()很相似,但是 recv()可以通过指定 flags 标志来控制如何接收数据,这些标志如下所 示:

通常一般我们将 flags 参数设置为 0,当然,你可以根据自己的需求设置该参数。
当指定 MSG_PEEK 标志时,可以查看下一个要读取的数据但不真正取走它,当再次调用 read 或 recv 函数时,会返回刚才查看的数据。
对于 SOCK_STREAM 类型套接字,接收的数据可以比指定的字节大小少。MSG_WAITALL 标志会阻 止这种行为,直到所请求的数据全部返回,recv 函数才会返回。对于 SOCK_DGRAM 和 SOCK_SEQPACKET 套接字,MSG_WAITALL 标志并不会改变什么行为,因为这些基于报文的套接字类型一次读取就返回整个 报文。
如果发送者已经调用 shutdown 来结束传输,或者网络协议支持按默认的顺序关闭并且发送端已经关闭, 那么当所有的数据接收完毕后,recv 会返回 0。
write函数
- 通过 write()函数可以向套接字描述符中写入数据,函数调用成功返回写入的字节数,失败返回-1,并设 置 errno 变量。
send()函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
send 和 write 很相似,但是 send 可以通过参数 flags 指定一些标志,来改变处理传输数据的方式。
即使 send()成功返回,也并不表示连接的另一端的进程就一定接收了数据,我们所能保证的只是当 send 成功返回时,数据已经被无错误的发送到网络驱动程序上。
close()关闭套接字
- 当不再需要套接字描述符时,可调用 close()函数来关闭套接字,释放相应的资源。
IP 地址格式转换函数
- 对于人来说,我们更容易阅读的是点分十进制的 IP 地址,譬如 192.168.1.110、192.168.1.50,这其实是 一种字符串的形式,但是计算机所需要理解的是二进制形式的 IP 地址,所以我们就需要在点分十进制字符 串和二进制地址之间进行转换。
- 点分十进制字符串和二进制地址之间的转换函数主要有:inet_aton、inet_addr、inet_ntoa、inet_ntop、 inet_pton 这五个,在我们的应用程序中使用它们需要包含头文件、以及<netinet/in.h>。
inet_aton、inet_addr、inet_ntoa 函数
- 这些函数可将一个 IP 地址在点分十进制表示形式和二进制表示形式之间进行转换,这些函数已经废弃 了,基本不用这些函数了,但是在一些旧的代码中可能还会看到这些函数。完成此类转换工作我们应该使用 下面介绍的这些函数。
inet_ntop、inet_pton 函数
- inet_ntop()、inet_pton()与 inet_ntoa()、inet_aton()类似,但它们还支持 IPv6 地址。它们将二进制 Ipv4 或 Ipv6 地址转换成以点分十进制表示的字符串形式,或将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址。使用这两个函数只需包含头文件即可!
- inet_pton()函数
int inet_pton(int af, const char *src, void *dst); |
inet_pton()函数将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址。
inet_pton()函数将点分十进制表示的字符串形式转换成二进制 Ipv4 或 Ipv6 地址。 将字符串 src 转换为二进制地址,参数 af 必须是 AF_INET 或 AF_INET6,AF_INET 表示待转换的 Ipv4 地址,AF_INET6 表示待转换的是 Ipv6 地址;并将转换后得到的地址存放在参数 dst 所指向的对象中,如果 参数 af 被指定为 AF_INET,则参数 dst 所指对象应该是一个 struct in_addr 结构体的对象;如果参数 af 被指 定为 AF_INET6,则参数 dst 所指对象应该是一个 struct in6_addr 结构体的对象。
inet_pton()转换成功返回 1(已成功转换)。如果 src 不包含表示指定地址族中有效网络地址的字符串, 则返回 0。如果 af 不包含有效的地址族,则返回-1 并将 errno 设置为 EAFNOSUPPORT。
inet_ntop()函数
inet_ntop()函数执行与 inet_pton()相反的操作
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); |
- 参数 af 与 inet_pton()函数的 af 参数意义相同。
- 参数 src 应指向一个 struct in_addr 结构体对象或 struct in6_addr 结构体对象,依据参数 af 而定。函数 inet_ntop()会将参数 src 指向的二进制 IP 地址转换为点分十进制形式的字符串,并将字符串存放在参数 dst所指的缓冲区中
- 参数 size 指定了该缓冲区的大小。 inet_ntop()在成功时会返回 dst 指针。如果 size 的值太小了,那么将会返回 NULL 并将 errno 设置为 ENOSPC。
使用例
- 服务端
|
- 客户端
|
























