Linux 系统是依靠互联网平台迅速发展起来的,所以它具有强大的网络功能支持,也是 Linux 系统的一 大特点。互联网对人类社会产生了巨大影响,它几乎改变了人们生活的方方面面,可见互联网对人类社会的重要性!
下面来学习 Linux 下的网络编程,我们一般称为 socket 编程,在网络基础知识中给大家介绍过,socket 是内核向应用层提供的一套网络编程接口,用户基于 socket 接口可开发自己的网络相关应用程序。
socket 简介
套接字(socket)是 Linux 下的一种进程间通信机制(socket IPC),在前面的内容中已经给大家提到过, 使用 socket IPC 可以使得在不同主机上的应用程序之间进行通信(网络通信),当然也可以是同一台主机上的不同应用程序。socket IPC 通常使用客户端服务器这种模式完成通信,多个客户端可以同时连接到服务器中,与服务器之间完成数据交互。
内核向应用层提供了 socket 接口,对于应用程序开发人员来说,我们只需要调用 socket 接口开发自己的应用程序即可!socket 是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口。在设计模式中, socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议隐藏在 socket 接口后面,对用户来说,一组简单的接口就是全部,让 socket 去组织数据,以符合指定的协议。所以,我们无需深入的去理解 tcp/udp 等各种复杂的 TCP/IP 协议,socket 已经为我们封装好了,我们只需要遵循 socket 的规定去编程,写出的程序自然遵循 tcp/udp 标准的。
当前网络中的主流程序设计都是使用 socket 进行编程的,因为它简单易用,它还是一个标准(BSD socket),能在不同平台很方便移植,比如你的一个应用程序是基于 socket 接口编写的,那么它可以移植到任何实现 BSD socket 标准的平台,譬如 LwIP,它兼容 BSD Socket;又譬如 Windows,它也实现了一套基于 socket 的套接字接口,更甚至在国产操作系统中,如 RT-Thread,它也实现了 BSD socket 标准的 socket 接口。
socket 编程接口介绍
向大家介绍,socket 编程中使用到的一些接口函数。使用 socket 接口需要在我们的应用程序 代码中包含两个头文件:
#include <sys/types.h>
#include <sys/socket.h>
socket()函数
socket()函数原型如下所示:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket()函数类似于 open()函数,它用于创建一个网络通信端点(打开一个网络通信),如果成功则返回一个网络文件描述符,通常把该文件描述符称 socket 描述符(socket descriptor ),这个 socket 描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。 该函数包括 3 个参数,如下所示:
domain
参数 domain 用于指定一个通信域;选择将用于通信的协议族。可选的协议族如下表所示:
协议族名字 | 说明 | 帮助信息 |
AF_UNIX | AF_LOCAL Local communication | unix(7) |
AF_INET | IPv4 Internet protocols | ip(7) |
AF_INET6 | IPv6 Internet protocols | ipv6(7) |
AF_IPX | IPX - Novell protocols | |
AF_NETLINK | Kernel user interface device | netlink(7) |
AF_X25 | ITU-T X.25/ISO-8208 protocol | x25(7) |
AF_AX25 | Amateur radio AX.25 protocol | |
AF_ATMPVC | Access to raw ATM PVCs | |
AF_APPLETALK | AppleTalk | ddp(7) |
AF_PACKET | Low level packet interface | packet(7) |
AF_ALG | Interface to kernel crypto API |
对于 TCP/IP 协议来说,通常选择 AF_INET 就可以了,当然如果你的 IP 协议的版本支持 IPv6,那么可以选择 AF_INET6。
protocol
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。在 AF_INET 通信域中,套接字类型为 SOCK_STREAM 的默认协议是传输控制协议(Transmission Control Protocol TCP 协议)。在AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。
调用 socket()与调用 open()函数很类似,调用成功情况下,均会返回用于文件 I/O 的文件描述符,只不过对于 socket()来说,其返回的文件描述符一般称为 socket 描述符。当不再需要该文件描述符时,可调用 close()函数来关闭套接字,释放相应的资源。 如果 socket()函数调用失败,则会返回-1,并且会设置 errno 变量以指示错误类型。
使用示例
int socket_fd = socket(AF_INET,SOKC_STREAM,0) //打开套接字
if(0 > socket_fd){
perror("socket error");
exit(-1);
}
......
close(socket_fd); //关闭套接字
bind()函数
bind()函数原型如下所示:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数用于将一个 IP 地址或端口号与一个套接字进行绑定(将套接字与地址进行关联)将一个客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。一般来讲,会将一个服务器的套接字绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址 (注意这里说的地址包括 IP 地址和端口号)。因为对于客户端来说,它与服务器进行通信,首先需要知道服务器的 IP 地址以及对应的端口号,所以通常服务器的 IP 地址以及端口号都是众所周知的。
调用 bind()函数将参数 sockfd 指定的套接字与一个地址 addr 进行绑定,成功返回 0,失败情况下返回-1,并设置 errno 以提示错误原因。 参数 addr 是一个指针,指向一个 struct sockaddr 类型变量,如下所示:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
第二个成员 sa_data 是一个 char 类型数组,一共 14 个字节,在这 14 个字节中就包括了 IP 地址、端口号等信息,这个结构对用户并不友好,它把这些信息都封装在了 sa_data 数组中,这样使得用户是无法对 sa_data 数组进行赋值。事实上,这是一个通用的 socket 地址结构体。
一般我们在使用的时候都会使用 struct sockaddr_in 结构体,sockaddr_in 和 sockaddr 是并列的结构(占用的空间是一样的),指向 sockaddr_in 的结构体的指针也可以指向 sockaddr 的结构体,并代替它,而且 sockaddr_in 结构对用户将更加友好,在使用的时候进行类型转换就可以了。该结构体内容如下所示:
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP 地址 */
unsigned char sin_zero[8];
};
这个结构体的第一个字段是与 sockaddr 结构体是一致的,而剩下的字段就是 sa_data 数组连续的 14 字 节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr 字段是我们需要填写的 IP 地址信息,剩下 sin_zero 区域的 8 字节保留未用。 最后一个参数 addrlen 指定了 addr 所指向的结构体对应的字节长度。
使用示例
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
注意,代码中的 htons 和 htonl 并不是函数,只是一个宏定义,主要的作用在于为了避免大小端的问题, 需要这些宏需要在我们的应用程序代码中包含头文件。
Tips:bind()函数并不是总是需要调用的,只有用户进程想与一个具体的 IP 地址或端口号相关联的时候才需要调用这个函数。如果用户进程没有这个必要,那么程序可以依赖内核的自动的选址机制来完成自动地址选择,通常在客户端应用程序中会这样做。
listen()函数
listen()函数只能在服务器进程中使用,让服务器进程进入监听状态,等待客户端的连接请求,listen()函数在一般在 bind()函数之后调用,在 accept()函数之前调用,它的函数原型是:
int listen(int sockfd, int backlog);
无法在一个已经连接的套接字(即已经成功执行 connect()的套接字或由 accept()调用返回的套接字)上执行 listen()。
参数 backlog 用来描述 sockfd 的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候,可能还存在其它的客户端请求建立连接,因为 TCP 连接是一个过程,由于同时尝试连接的用户过多,使得服务器进程无法快速地完成所有的连接请求,那怎么办呢?直接丢掉其他客户端的连接肯定不是一个很好的解决方法。因此内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,这样的一个队列内核不可能让其任意大,所 以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。