该系列文章总纲链接:专题分纲目录 LinuxC 系统编程
本章节思维导图如下所示(思维导图会持续迭代):
第一层:

第二层:
1 套接字概念
linux使用套接字进行进程间的通信;通过套接字,其他进程的位置对于应用程序来讲是透明的;套接字代表通信的端点,必须保证2个端点各有一个套接字才可以。套接字的通信过程如下:
套接字实现了一层抽象,让用户感觉在操作文件一样。抽象过程如下:
2 准备工作
2.1 字节序
在网络环境中,进程间通信是跨主机的,因此就有了字节序不统一的问题。为解决这个问题,网络协议提供一种字节序,当跨主机的两个进程进行通信时,先将需要传输的数据转换成网络字节序,待接收方接收数据后,将其转换为本机的字节序。字节序转换流程如下:
linux环境下使用4个函数进行字节序的转换,函数原型如下:
详细见linux函数参考手册。网络字节序就是大端字节序,但是网络的情况是复杂的,为了保证代码的可移植性,不论在哪种字节序的主机上都要做字节转换处理。
2.2 地址格式
网络环境中每台计算机都有一个IP地址。(对于IPv4协议来讲,是一个32位无符号整数;对于IPv6协议来讲,是一个128位无符号整数)。linux中使用in_addr结构表示一个IP地址,结构定义如下:
当确定了目标机后,还需要通过端口号来确定主机中哪个进程需要通信(每个进程对应一个16位的端口号)。因此,在网络中,一个IP地址与一个端口号连在一起就可以确定一台主机的一个进程。当唯一的两点已经确认后,通信就开始了。linux中地址结构的定义如下:
结构socketaddr_in与socketaddr等长,所以可以很容易地相互转换。
2.3 地址形式转换
IP地址是以二进制的形式存储在地址结构中的,直接观察有些不便,用点分十进制(xxx.xxx.xxx.xxx)表示才直观。linux下提供的IP地址转换函数如下:
详细见linux函数参考手册。
2.4 获得主机信息
一台主机和网络相关的信息一般存放在系统中的某个文件里(例如/etc/hosts),用户可以通过系统函数读取文件上的内容,在linux下使用gethostent函数读取和主机有关的信息:
详细见linux函数参考手册。其中,hostent结构体的定义如下:
注意:调用gethostent两次,则第一次host指针指向的缓冲区内容会被冲掉。
2.5 地址映射
对于用户而言,套接字的地址结构信息是不必要的,用户只需传递一个sockaddr_in地址结构的地址,之后由系统来填充其中的内容即可。网络环境中的服务器需要提供一个唯一地址的IP和主机名(域名);对于大部分服务器来讲,客户端不知道其IP地址,但知道其域名。DNS可以将域名转换为IP地址,转换过程如下:
转换后的IP地址和端口号存储在addr_info信息结构中。linux下提供一个函数,即根据服务器的域名和服务名称即可得到服务器的IP地址和端口号;并将其填写到一个sockaddr_in地址结构中,该函数内部访问了DNS服务器,从而得到需要访问主机的IP号和端口号,函数原型如下:
详细见linux函数参考手册。
3 套接字基础编程
套接字技术对大部分通信细节做了隐藏,使得操作类似于文件,也正因为这样,所以很多文件操作函数也可以用在套接字上。(linux将设备抽象为文件的策略使得编程简单很多)
3.1 建立和销毁套接字描述符
linux环境下创建一个套接字和取消一个套接字的函数原型如下:
详细见linux函数参考手册。
3.2 地址绑定
创建一个套接字以后需要绑定地址的套接字才能够进行通信。linux下使用bind函数将一个套接字绑定在一个地址上,函数原型如下:
详细见linux函数参考手册。注意:sockaddr_in结构中不能指定协议为IPv6,即通信域不能指定为AF_INET6。其中,第二个参数在实际当中需要先对参数进行初始化,过程如下:
3.3 建立一个连接
在绑定一个套接字后,客户端就可以建立一个连接,对于面向服务的套接字类型,必须指定;对于无连接服务,这一步是没有必要的。linux环境下使用connect函数建立一个主动建立一个连接,函数原型如下:
详细见linux函数参考手册。注意:对于网络而言,应用程序一定要能处理连接时可能发生的错误;失败原因有很多,一旦失败就要考虑重新尝试,不过尝试一般都需要有一段时间的延迟,以保证网络有时间自动恢复。
使用connect函数的机制如图所示:
客户端建立一个连接,服务器端就要监听并接受这样一个连接,进而对其进行处理,linux下使用的listen函数监听客户端的连接请求;使用accept函数接受一个连接的请求,函数原型如下:
详细见linux函数参考手册。注意:对于套接字描述符,不可使用lseek函数对其进行重定位。
3.4 使用文件读写函数读写套接字
在网络中使用read/write函数容易出现问题,原因如下:
延时问题:对于本地文件夹,字节流在本地传输的延时可以忽略不计,但是在网络中传输的时间可能会很长;因此会造成I/O的阻塞;解决方法只能是非阻塞/使用多路I/O。
网络应用程序要能够处理因为中断/网络连接问题造成的读写操作异常返回,但是这样会让程序变得更阿基复杂和不好控制。
注意:close函数在网络环境下出错的原因并不是文件本身的问题,而是由于“缓输出”导致了异常;write函数只是将要写入文件的内容放到缓存中,真正写到外存上是需要时间的,对于本地文件,几乎不会出错,但是在网络环境下,出错的概率就大了;因此,在网络环境下,调用write函数并不能保证文件内已经准确到达对端。
3.5 面向连接的数据传输
linux环境下用read/write函数进行网络通信很容易出问题,但是linux下有专门用于面向连接的套接字的函数,这两个函数分别是send和recv,其函数原型如下:
3.6 面向连接的最简单服务器端与客户端流程
@1 服务器端执行流程(伪代码)如下:
@2 客户端执行流程(伪代码)如下:
注意:
在实际当中,如果不知道服务器的IP地址,可以使用getaddrinfo函数,通过DNS服务器将服务器的域名转换为服务器主机的IP;如果连域名也不知道,那就无法通信。
对于一般可间的局域网,服务器和客户端大都属于一个用户组,其IP地址是相互可见的;但是对于互联网环境中,服务器的IP往往是对客户隐藏的。
3.7 面向无连接的数据传输
用于面向无连接套接字的读写函数要复杂一点,由于没有建立一个连接,所以每次发送数据的过程都要明确指出该数据包的目的地址;在接收数据包时,接收进程可以得到发送该数据包的地址。linux环境下提供专门对无连接套接字进行读写的函数,分别是sendto和recvfrom函数,函数原型如下:
3.8 面向无连接的最简单服务器端与客户端流程
@1 服务器端执行流程(伪代码)如下:
@2 客户端执行流程(伪代码)如下:
4 非阻塞套接字
当进程需要对套接字进行读写操作,而套接字的数据尚未准备好,则进行读写套接字操作的函数将会阻塞,使进程进入休眠状态等待,其后面的操作也就无法进行了,非阻塞I/O将解决这种问题。由于套接字属于一种特殊的文件,因此,可以使用更改文件阻塞的方式来修改套接字的阻塞状态。在服务器端执行流程(伪代码)如下:
非阻塞网络应用程序的客户端流程与之前的可以一致,也可以将其做成输入阻塞,以便于验证服务端的正确性。