Socket网络编程

一.网络通信

 

二.Socket编程

 

 

一.网络编程

 

  大部分的网络应用系统可以分为两个部分:客户(Client)和服务器(Server),而网络服务程序架构有两种一种是CS模式,
一种是BS模式:
  CS即Client/Server(客户机/服务器)结构,C/S结构在技术上很成熟,它的主要特点是交互性强、具有安全的存取模式、网
络通信量低、响应速度快、利于处理大量数据。但是该结构的程序是针对性开发,变更不够灵活,维护和管理的难度较大。并
且,由于该结构的每台客户机都需要安装相应的客户端程序,分布功能弱且兼容性差,不能实现快速部署安装和配置,因此缺少
通用性,具有较大的局限性。要求具有一定专业水准的技术人员去完成。譬如SecureCRT、迅雷、QQ这些就是CS模式,需要安
装客户端软件才能访问服务器。
  BS即Browser/Server(浏览器/服务器)结构,就是只安装维护一个服务器(Server),而客户端采用浏览器(Browse)运行
软件。B/S结构应用程序相对于传统的C/S结构应用程序是一个非常大的进步。 B/S结构的主要特点是分布性强、维护方便、开发
简单且共享性强、总体拥有成本低。但数据安全性问题、对服务器要求过高、数据传输速度慢、软件的个性化特点明显降低,这
些缺点是有目共睹的,难以实现传统模式下的特殊功能要求。例如通过浏览器进行大量的数据输入或进行报表的应答、专用性打
印输出都比较困难和不便。此外,实现复杂的应用构造有较大的困难。那像网站、学校教务系统等这些通过浏览器访问的模式叫
BS模式。
 

 

OSI七层模型

 

  OSI(Open System Interconnection,开放系统互联)七层网络模型是ISO(International Organization for

 

Standardization,国际标准化组织)提出的一个参考模型,是一个把网络通信在逻辑上的定义,也可以理解成为定义了通用的网

 

络通信规范。而我们的数据在网络中传输的过程,实际上就是如下图的封装和解封装的过程,发送方通过各种封装处理,把数据

 

转换成比特流的形式,比特流在信号传输的硬件媒介中传输,接收方再把比特流进行解封装处理。

 

 

 

1.物理层
  规定了如何为网络通信实现最底层的物理连接,以及物理设备的机械、电气、功能和过程特性。如:如何使用电缆和接头的
类型、用来传送信号的电压等。需要注意的是,网络通信过程中所需的物理媒介(网线、线缆等),其实并不属于物理层,因为物
理层实际上是一种规定,规定这些物理媒介设备在连接网络时的各种规格、参数以及工作方式。但是同时,双绞线、线缆等物理
媒介又是物理层的实现。
2.数据链路层
  规定了如何进行物理地址寻址、如何在物理线路上进行数据(帧frame)的可靠传递以及流量控制。数据链路层协议有SLIP协
议、CSLIP协议、PPP协议等。交换机,对帧解码并根据帧中包含的信息把数据发送到正确的接收方,所以交换机是工作在数据
链路层的。
3.网络层
  规定了通过哪些网络节点、什么样的网络路径来将数据(数据包)从发送方发送到接收方。在网络层中,确定了从节点A发数据
到节点B的网络路径,经过哪些节点。网络层既可以建立LAN通信系统,更主要的是可以在WAN网络系统中建立通信,这是因为
它有自己的路由地址结构,通过路由协议(又称可路由协议)进行网络通信的路由工作。
4.传输层
  负责总体的数据传输和数据控制,提供端到端的交换数据的机制。传输层对数据(段)进行分割和重组,并且进行流量控制和
根据接收方的接收数据能力确定适当的传输速率。例如以太网无法处理大于1500字节的数据包,传输层将数据分割成数据片段,
并对小数据片段进行序列编号。接收方的传输层将根据序列编号对数据进行重组。传输层协议有TCP协议、UDP协议等。
5.会话层
  在网络中的两个节点之间建立、维持和终止通信。
6.表示层
  在应用程序和网络之间对数据进行格式化,使之能够被另一方理解。即发送方的表示层将应用程序数据的抽象语法转换成网
络适用于OSI网络传输的传送语法,接收方则相反。除此之外,表示层还可对数据进行加密与解密。
7.应用层
  最顶层的OSI层,为应用程序提供网络服务。如为电子邮件、文件传输功能提供协议支持。应用层协议有HTTP协议、FTP协
议、SMTP协议等。
 
TCP/IP协议
  虽然OSI参考模型是国际化标准组织提出了的一个标准,但由于他定义的太过复杂而错过了时间节点,让TCP/IP协议成为了计
算机网络通信的事实标准。在今天的基于TCP/IP的互联网诞生之前,能够使用接口通信处理实现互联互通的电脑并不多,而且大
部分电脑之间信息的交换并不兼容。后来好几个牛逼哄哄的歪果仁开始捣鼓一些协议,能够让电脑之间进行通信。终于在1974年
12月,Bob Kahn和Vinton G.Cerf带领的团队首先制定出了通过详细定义的TCP/IP协议标准。当时作了一个试验,将信息包通
过点对点的卫星网络,再通过陆地电缆,再通过卫星网络,再由地面传输,贯串欧洲和美国,经过各种电脑系统,全程9.4万公
里竟然没有丢失一个数据位,远距离的可靠数据传输证明了TCP/IP协议的成功。1983年1月1日,运行较长时期曾被人们习惯了
的NCP被停止使用,TCP/IP协议作为因特网上所有主机间的共同协议,从此以后被作为一种必须遵守的规则被肯定和应用。
  从“TCP/IP”名字上来看,貌似这只是tcp协议和ip协议,但是实际上,这是很多协议很多协议组成的一个协议集合,我们
把这集合统称为 TCP/IP协议族,简称为TCP/IP协议。对于TCP/IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层和
数据链路层。通过下图我们可以了解TCP/IP协议结构并对比了解学习TCP/IP 四层参考模型和 OSI 七层参考模型,因为TCP/IP太
过简单,我们也会使用模型化的TCP/IP五层模型来描述计算机网络。

 

 

 

 

 

 

 

TCP/IP——网络接口层
 
  主要作用一:数据封装/解封装成帧(frame)。为了保证可靠传输,网络层传过来的数据在这里被加工成了可被物理层传输的结
构包——帧。帧中除了包括需要传输的数据外,还包括发送方和接收方的物理地址以及检错和控制信息。其中的物理地址确定了
帧将发送到何处,检错和控制信息则是用来保证数据的无差错到达。数据帧结构如下(Address均为mac地址):

 

 

 

主要作用二:控制帧传输。控制帧的传输主要体现在反馈重发、计时器、帧序号方面。接收方通过对帧的差错编码(奇偶校验码
或 CRC 码)的检查,来判断帧在传输过程中是否出错,并向发送发进行反馈,如果传输发生差错,则需要重发纠正。作为发送
发,如果在发送帧后,会同时启动定时器,如果帧发送后在一定时间内没有收到反馈,为了避免传输停滞不前,则在计时器
Timeou后认为帧传输出错,自动重发。为了避免多次收到同一帧并将其递交给网络层的情况发生,则需要对每个发送的帧进行
编号,接收方以此来判断该帧是否重复接受了。
主要作用三:流量控制。由于收发双方各自使用的设备工作速率和缓冲存储空间的差异,可能出现发送方的发送能力大于接收
方接收能力的现象,此时若不对发送方的发送速率做适当的限制,前面来不及接收的帧将被后面不断发送来的帧“淹没”,从而
造成帧的丢失而出错。由此可见,流量控制实际上是对发送方数据流量的控制,使其发送速率不超过接收方的速率。所以需要一
些规则使得发送方知道在什么情况下可以接着发送下一帧,而在什么情况下必须暂停发送,以等待收到某种反馈信息后再继续发
送。

 

TCP/IP——网络层

 

  说到网络层不得不提的就是IP协议,它是TCP/IP协议族中最为核心的协议。所有的TCP、UDP、ICMP、IGMP协议数据都以IP

 

数据报格式传输。IP协议提供的是不可靠的、无连接的数据报传输服务。不可靠是指IP协议不会保证数据报能否成功到达目的

 

地,仅提供传输服务,传输出错,则会丢弃出错的数据报。无连接是指IP协议对数据报的处理是独立的,这也意味着接收方不一

 

定会按照发送顺序接收数据报。IP数据报格式如下:

 

 

 

 

 

子网划分:
 
一个有500台主机的网络,如果使用C类地址,则无法满足主机数量需求,使用B类地址则会造成IP地址的浪费。由此,需要在
ABC类网络的基础上进行子网划分:即占用主机号的前几位表示子网号。子网掩码的概念由此被引入。

 

 

子网掩码中的 1 标识了 IP 地址中相应的网络号,0 标识了主机号。将 IP 地址和子网掩码进行逻辑与运算 ,结果就能得到网络号和子网
号。
 
TCP/IP——传输层
 
无论参考OSI还是TCP/IP的网络模型,我们从传输层向更底层看,各层的协议都是在直接或间接的服务于主机与主机之间的通
信,而传输层则是在进程与进程通信层面上的。传输层有两个重要的协议——TCP(Transmission ControlProtocol,传输控制
协议)和UDP(User Data Protocol,用户数据报协议)。
UDP协议:
  UDP(User Datagram Protocol)即用户数据报协议,其传输机制决定了它的最大优点——快,同时也决定了它最大的缺点
——不可靠、不稳定。
  UDP是无连接的,发送数据之前不需要建立连接(TCP需要)。减少了开销和延时。
  UDP是面向报文的,对IP数据报只做简单封装(8字节UDP报头)。减少报头开销。
  UDP没有阻塞机制,宁愿阻塞时丢弃数据不传,也不阻塞造成延时。
  UDP支持一对一、一对多、多对一、多对多通信。
    
UDP报文结构:

 

 

 

 

TCP协议:
  TCP(Transmission Control Protocol)传输控制协议,相对于UDP,TCP是面向连接的、提供可靠的数据传输服务。同时也
是较UDP开销较大的、传输速度较慢的。
  TCP提供可靠的、面向连接的数据传输服务。使用TCP通信之前,需要进行“三次握手”建立连接,通信结束后还要使
用“四次挥手”断开连接。
  TCP是点对点的连接。一条TCP连接只能连接两个端点。
  TCP 提供可靠传输,无差错、不丢失、不重复、按顺序。
  TCP 提供全双工通信,允许通信双方任何时候都能发送数据,发送方设有发送缓存,接收方设有接收缓存。
  TCP 面向字节流 。TCP 并不知道所传输的数据的含义,仅把数据看作一连串的字节序列,它也不保证接收方收到的数据块
和发送方发出的数据块具有大小对应关系。
TCP报文结构:
 
 
 
 
 
 
 TCP是面向字节流的,通过 TCP 传送的字节流中的每个字节都按顺序编号,而报头中的Sequence Number字段值则指的是本
报文段数据的第一个字节的序号。Acknowledgment Number是期望收到对方下个报文段的第一个数据字节的序号。
    Offset:占4位,指 TCP 报文段的报头长度,包括固定的20字节和TCP Options字段。
    Reserved:占6位,保留为今后使用,目前为0。
    TCP flags的C、E、U、A、P、R、S、F字段用来说明该报文的性质。意义如下:
       C(CWR)和E(ECE)用来支持ECN(显示阻塞通告)。
       U(URGENT):当 URG=1时,它告诉系统此报文中有紧急数据,应优先传送(比如紧急关闭),这要与紧急指针字段
配合使用 。
       A(ACK):仅当 ACK=1时确认号字段才有效。建立 TCP 连接后,所有报文段都必须把 ACK 字段置为 1。
       P(PUSH):若TCP连接的一端希望另一端立即响应,PSH字段便可以“催促”对方,不再等到缓存区填满才发送。
         R(RESET):若 TCP 连接出现严重差错,RST 置为 1,断开 TCP 连接,再重新建立连接。
         S(SYN):用于建立和释放连接,当SYN=1时,表示建立连接。
         F(FIN):用于释放连接,当 FIN=1,表明发送方已经发送完毕,要求释放TCP 连接。
    Window:占2个字节。窗口值是指发送者自己的接收窗口大小,因为接收缓存的空间有限。
    CheckSum:占2个字节。和UDP报文一样,有一个检验和,用于检查报文是否在传输过程中出差错。
    Urgent Pointer:占2字节。当URG=1时才有效,指出本报文段紧急数据的字节数。
 
TCP建立连接的三次握手:
 

 

 

1.   Client首先向Server发送连接请求报文段,同步自己的seq(x),Client进入SYN_SENT状态。
2.   Server收到Client的连接请求报文段,返回给Client自己的seq(y)以及ack(x+1),Server进入SYN_REVD状态。
3.   Client收到Server的返回确认,再次向服务器发送确认报文段ack(y+1),这个报文段已经可以携带数据了。Client进
 入ESTABLISHED状态。
4.   Server再次收到Client的确认信息后,进入ESTABLISHED状态。
  TCP连接至此建立起来了。为什么要做三次握手呢?握手的过程实际上是在通知对方自己的初始化序号(Initial Sequence
Number),简称ISN,也就是上图中的x和y。x和y会被当作之后传输数据的一个依据,以保证TCP报文在传输过程中不会混乱。
  我们回到TCP Header结构来看,Sequence Number和Acknowledgment Number都是占32位,所以seq和ack的取值范
围是0 ~ 2^32-1。seq和ack每增加到2^32-1,则重新从0开始。值得一提的是,seq的初始值(ISN)并不是每次都从0开始的。
我们设想一下,如果是从0开始,那么当TCP三次握手建立连接完成后,Client发送了30个报文,然后Client断线了。于是Client
重连,再次用0作为初始的seq,这样就会出现两个报文具有相同的seq,就出现了混乱。事实上TCP的做法是每隔4微秒就对ISN
做一次加1操作,当ISN到达2^32-1后再次从0开始的时候,已经过去了几个小时,之前的seq=0的报文已经不存在于这次连接中
了,这样就避免了上面的问题。
 
TCP断开连接的四次握手:
 

 

 

       (1)Client向Server发送断开连接请求的报文段,seq=m(m为Client最后一次向Server发送报文段的最后一个字节序号加1),
Client进入FIN-WAIT-1状态。
  (2)Server收到断开报文段后,向Client发送确认报文段,seq=n(n为Server最后一次向Client发送报文段的最后一个字节序
号加1),ack=m+1,Server进入CLOSE-WAIT状态。此时这个TCP连接处于半开半闭状态,Server发送数据的话,Client仍然可
以接收到。
  (3)Server向Client发送断开确认报文段,seq=u(u为半开半闭状态下Server最后一次向Client发送报文段的最后一个字节序
号加1),ack=m+1,Server进入LAST-ACK状态。
  (4)Client收到Server的断开确认报文段后,向Server发送确认断开报文,seq=m+1,ack=u+1,Client进入TIME-WAIT状
态。
  (5)Server收到Client的确认断开报文,进入CLOSED状态,断开了TCP连接。
  (6)Client在TIME-WAIT状态等待一段时间(时间为2*MSL((Maximum Segment Life)),确认Client向Server发送的最后一次
断开确认到达(如果没有到达,Server会重发步骤(3)中的断开确认报文段给Client,告诉Client你的最后一次确认断开没有收
到)。如果Client在TIME-WAIT过程中没有再次收到Server的报文段,就进入CLOSES状态。TCP连接至此断开。
  
  TCP连接可靠性的体现:
  (1)TCP报文段的长度可变,根据收发双方的缓存状态、网络状态而调整。
  (2)当TCP收到发自TCP连接另一端的数据,它将发送一个确认。
  (3)当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报
文段。
  (4)TCP将保持它首部和数据的检验和。如果通过检验和发现报文段有差错,这个报文段将被丢弃,等待超时重传。
  (5)TCP将数据按字节排序,报文段中有序号,以确保顺序的正确性。
  (6)TCP还能提供流量控制。TCP连接的每一方都有收发缓存。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数
据。这将防止较快主机致使较慢主机的缓冲区溢出。
  需要注意的是,TCP报文传输采用接受后返回确认的方式来保证报文传输的可靠性,并不是意味着发送方在发送一个报文段
后就进入等待确认状态,让后面的报文段等着。也不是接收方在接收到一个报文后,对每一个报文都进行回复确认。
  真实的情况是,对于发送方,在发送一个报文段后,复制一份该报文段的副本,然后继续进行下一个报文段的发送,如果没
有得到发送方的回复确认,就对该报文段进行超时重发。对于接收方来说,则采用“积累确认”的方式进行回复。接收者收到多
个连续的报文段后,只回复确认最后一个报文段,表示在这之前的数据都已收到。以此达到提升传输效率的目的.
 
 
 
TCP/IP——应用层
 关于应用层协议,不同的应用或者具体来说同一种应用不同的需求,都会使用不同的应用层协议。下面是常见的应用层协议使
用的端口:

 

 

  因为端口号是16位的。这也就意味着端口号的范围是0~65535。其中1~1024是被RFC3232规定好了的,被称作“众所周知
的端口”(Well Known Ports),监听该范围内端口的程序必须以root权限运行;从1025~65535的端口被称为动态端口
(Dynamic Ports),可用来建立与其它主机的会话,也可由用户自定义用途。所以我们在写服务器程序时,一般使用该范围内
的端口,当然应该尽量避免一些知名的端口或当前系统正在使用的端口。具体怎么知道当前主机上运行了哪些服务程序并监听了
哪些端口呢?我们可以使用netstat命令查看:
 

 

 

  一台服务器主机可以提供多种服务,譬如一台Linux主机可以提供SSH服务,这样我们就可以使用SSH协议的客户端软件(如
SecureCRT,Putty等)来远程登录控制;当然也可以在该Linux主机上同时提供web服务,这样能通过浏览器访问架设在该主机
上的网站。对于每一个服务,都会有一个相应的程序在后台默默地运行着提供相应的服务,其中openssh或dropbear就是实现
ssh协议的服务程序,apache、nginx、httpd、boa、goahead、appweb等是提供web访问的web服务器,vsfptd和pureftpd
就是用来提供FTP访问的服务器程序。既然同一台服务器可以提供这么多服务,那么当某个客户端程序访问服务器的时候,系统
怎么知道该客户端是访问哪个服务器程序,这也决定了从网卡上收到的数据包究竟该给哪个服务器程序。其实,每一个服务器程
序都会监听一个特定的端口,并且这个端口不可被其他程序重用,同时每一个客户端在访问某个服务器时,除了指定服务器的地
址(包括域名和IP地址)还需要说明要访问哪个端口,那么从该客户端过来的数据系统会给监听相应端口的服务器程序。 
  
  在很多客户端软件上,如果没有指定端口的话,会使用默认的端口。譬如我们使用浏览器上网时,通常只是输入域名(如
www.baidu.com),而并没有给出端口?事实上这是因为浏览器会默认访问80端口。而如果想访问某个特定的端口,我们可以
在域名后添加相应的端口来访问,如:https://baidu.com:443,用该地址可以访问百度服务器的443端口。在这里的实例中,
www指定是http协议,https指定是https协议,baidu.com指定百度服务器的域名,系统会查询DNS将该域名转换成IP地址,而
:443 则指定访问的端口,SSL默认使用443端口。
  接下来,我们以下图为例,讲解客户端(192.168.0.3) 通过浏览器访问百度服务器(baidu.com)的过程,在这里我们不讨论
HTTP协议,而只是以“你好,百度!”代表相应的应用层数据。
 

 

客户端发送数据给服务器的流程:

 

1, PC上浏览器希望发送“百度,你好!”这个数据给百度服务器,因为百度服务器的IP地址(220.181.57.216)不好记,所以我
们访问某个服务器时通常都不是使用IP地址,而是域名(baidu.com)。但在计算机网络TCP/IP协议栈中是通过IP地址来标识每一
个结点的(包括主机、无线路由器等),这时我们需要一种机制将域名翻译成相应的IP地址,这个工作就是DNS。所以
PC(192.168.0.13)首先要给域名服务器(如114.114.114.114)发送一个域名解析请求,将百度的域名baidu.com 转换成相应的IP
地址220.181.57.216。所有的Linux系统,域名服务器都在 /etc/resolv.conf 配置文件中指定。如果系统能聊QQ但不能通过域
名访问网页,就要检查DNS的设置是否正确;
2, 浏览器发出“百度,你好!”的数据报文,该数据报文通过系统调用进入到操作系统内核里的TCP/IP协议栈,首先进入到
TCP层会在应用层发过来的数据上加入20字节的TCP头,在TCP头部包含两个重要的信息,即源端口和目的端口。其中目的端口
是百度的web服务器监听的80端口,而源端口如果没设定的话就由操作系统内核动态分配一个未使用的端口,如上图中的
8888;
3, 紧接着数据报文走到下层IP层,该层会在TCP数据包的基础上再加一个20字节的IP头,在IP头里有两个非常重要的信息,即
源IP和目的IP,这里的源IP就是客户端PC的IP 192.168.0.13,而目的IP就是百度服务器的IP地址 220.181.57.216;
4, 数据包到达物理层以二进制形式通过电磁信号发送出去,因为在该报文中出现的目的IP地址(220.181.57.216)和源IP地址
(192.168.0.13)不在一个网段内,则计算机会将该数据包发送给默认网关,也就是PC所连的路由器;
5, 无线路由器收到来自PC的数据包,因为源IP 192.168.0.13是个私有IP,不能出现在Internet公网上,所以他会把数据包中的
私有IP(192.168.0.13)替换成自己的公网IP(110.110.110.110),同时将源端口(8888)也转换成自己的临时端口 9999,
并记录这条转换记录到NAT表中去;
6, 经过局域网上的路由器做了NAT转换之后的源IP和目的IP都是公网IP了,这时候就通过Internet上的节点转发到百度的服务
器上;
7, 百度的服务器在网卡上接收到数据包后,操作系统内核的IP层首先检查数据包里的目的IP是不是本机IP,如果不是就将数据
包直接丢掉;否则就会去掉20字节的IP头后扔给上层的TCP层继续处理;
8, 在TCP层会查看该数据包里的端口号是多少,经查询目的端口号是80,而刚好百度服务器上的web服务器程序nginx监听着
该端口。这时他首先将20字节的TCP头去掉,获取应用层数据“你好,百度!”,然后将该数据传给nginx服务程序。
 
服务器回复数据给客户端的流程:
9, 百度服务器在收到客户端的数据之后,假设需要给客户端回复数据“你好,客户端!”,该数据将会进入到操作系统内核的
TCP/IP协议栈,在TCP层将之前的源端口(9999)设置成目的端口,并将目的端口(80)设置成源端口;
10,紧接着数据报文走到下层IP层,该层也将之前的源IP(110.110.110.110)改成目的IP,并将目的IP(220.181.57.216)改
成源IP;
11,数据报文到达物理层通过无线电发送给相应的无线路由器,并经过Internet到达对端的无线路由器;
12, 无线路由器就相当于一个快递员,他只是负责转发这些数据并不接收并处理数据,在同一局域网下有很多电脑,这时他该
把数据发给谁呢?这时会查询之前数据发送出去记录的NAT表,这时候会发现之前的源IP和源端口 192.168.0.13:8888被替换成
了110.110.110.110:9999,那么现在到110.110.110.110:9999的数据报文就该给主机 192.168.0.13 的8888端口。路由器会
将该数据包中的目的IP和目的端口换掉,然后发送给相应的主机;
13, 主机192.168.0.13接收到路由器发送过来的数据后,在IP层验证IP地址是否是本机IP,如果是本机IP则去掉20字节的IP头并
给TCP层继续处理,TCP层确认目的端口号(8888)后去掉20字节的TCP头,并将服务器的数据“你好,客户端!”发给使用该
端口的浏览器。
14, 浏览器收到服务器端的数据后就解析HTTP数据并显示出来,完成整个服务器访问的流程。
 
   从上面的整个流程中,我们会发现一个TCP的网络链接中求包含一个四元组,即: 源IP、目的IP 和 源端口、目的端口。这就
像我们收发快递一样要有发件人和发件人地址、收件人和收件人地址一样。在理解上面的计算机网络通信的工作原理之后,接下
来我们就可以更好的理解Linux下的网络socket程序开发了。 
 
 
 
二、socket编程
 
socket通信简介
 
  网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“端口”可以唯一标识主机中的应用程序(进程)。这样利用二
元组(ip地址,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。使用TCP/IP协议
的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,几乎所有的应用
程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。 TCP/IP协议
族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。

 

 

Socket服务器和客户端示例代码
 
 
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define LISTEN_PORT 8889
#define BACKLOG 13
 
 
int main(int argc, char **argv)
{
  int rv = -1;
  int listen_fd, client_fd = -1;
  struct sockaddr_in serv_addr;
  struct sockaddr_in cli_addr;
  socklen_t cliaddr_len;
  char buf[1024];
 
  listen_fd = socket(AF_INET, SOCK_STREAM, 0);
  if(listen_fd < 0 )
  {
    printf("create socket failure: %s\n", strerror(errno));
    return -1;
  }
  printf("socket create fd[%d]\n", listen_fd);
 
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(LISTEN_PORT);
  serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  if( bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0 )
  {
    printf("create socket failure: %s\n", strerror(errno));
    return -2;
  }
  printf("socket[%d] bind on port[%d] for all IP address ok\n", listen_fd, LISTEN_PORT);
 
  listen(listen_fd, BACKLOG);
 
  while(1)
  {
    printf("\nStart waiting and accept new client connect...\n", listen_fd);
    client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cliaddr_len);
    if(client_fd < 0)
      {
        printf("accept new socket failure: %s\n", strerror(errno));
        return -2;
       }
    printf("Accept new client[%s:%d] with fd [%d]\n", inet_ntoa(cli_addr.sin_addr),
    ntohs(cli_addr.sin_port), client_fd);
 
  memset(buf, 0, sizeof(buf));
  if( (rv=read(client_fd, buf, sizeof(buf))) < 0)
  {
  printf("Read data from client socket[%d] failure: %s\n", client_fd, strerror(errno));
  close(client_fd);
  continue;
  }
  else if( rv == 0 )
  {  
    printf("client socket[%d] disconnected\n", client_fd);
    close(client_fd);
    continue;
  }
  printf("read %d bytes data from client[%d] and echo it back: '%s'\n", rv, client_fd, buf);
  if( write(client_fd, buf, rv) < 0 )
  {
  printf("Write %d bytes data back to client[%d] failure: %s\n", rv, client_fd,
  strerror(errno));
  close(client_fd);
  }
 
  sleep(1);
  close(client_fd);
  }
  close(listen_fd);
  }
 
 
 
 
 
 
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8889
#define MSG_STR "Hello, Unix Network Program World!"
 
int main(int argc, char **argv)
{
  int conn_fd = -1;
  int rv = -1;
  char buf[1024];
  struct sockaddr_in serv_addr;
 
  conn_fd = socket(AF_INET, SOCK_STREAM, 0);
  if(conn_fd < 0)
  {
    printf("create socket failure: %s\n", strerror(errno));
    return -1;
  }
 
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(SERVER_PORT);
  inet_aton( SERVER_IP, &serv_addr.sin_addr );
  if( connect(conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
  {
    printf("connect to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
    return 0;
  }
 
  if( write(conn_fd, MSG_STR, strlen(MSG_STR)) < 0 )
  {
    printf("Write data to server [%s:%d] failure: %s\n", SERVER_IP, SERVER_PORT, strerror(errno));
    goto cleanup;
  }
  memset(buf, 0, sizeof(buf));
  rv = read(conn_fd, buf, sizeof(buf));
  if(rv < 0)
  {
    printf("Read data from server failure: %s\n", strerror(errno));
    goto cleanup;
  }
  else if( 0 == rv )
  {
    printf("Client connect to server get disconnected\n");
    goto cleanup;
  }
  printf("Read %d bytes data from server: '%s'\n", rv, buf);
  cleanup:
  close(conn_fd);
}
 
 
 
socket操作API函数
 
  既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为
例,介绍几个基本的socket接口函数。
 
socket()函数
 
int socket(int domain, int type, int protocol);
 
  socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符
(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参
数,通过它来进行一些读写操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数
分别为:
  domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,
Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了
要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、
SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、
IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不
是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默
认协议。
 
 
bind()函数
 
  当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个
具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如
ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身
的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生
成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
 
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 
函数的三个参数分别为:
 
  sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一
  个名字。
  
  addrlen:对应的是地址的长度。
  
  addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地
  址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核:
 
 
通用套接字 sockaddr 类型定义:
 
typedef unsigned short int sa_family_t;
struct sockaddr
{
  sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
  char sa_data[14]; /* 14 bytes of protocol address */
}
 
ipv4对应的是sockaddr_in类型定义:
 
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr
{
  uint32_t s_addr;
};
struct sockaddr_in
{
  sa_family_t sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
  in_port_t sin_port; /* 2 bytes port*/
  struct in_addr sin_addr; /* 4 bytes IPv4 address*/
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[8];
/* 8 bytes unused padding data, always set be zero */
};
ipv6对应的sockaddr_in6类型定义:
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
  union
{
  uint8_t __u6_addr8[16];
  uint16_t __u6_addr16[8];
  uint32_t __u6_addr32[4];
} __in6_u;
}
struct sockaddr_in6
{
  sa_family_t sin6_family; /*2B*/
  in_port_t sin6_port; /*2B*/
  uint32_t sin6_flowinfo; /*4B*/
  struct in6_addr sin6_addr; /*16B*/
  uint32_t sin6_scope_id; /*4B*/
};
Unix域对应的sockaddr_un类型定义:
#define UNIX_PATH_MAX 108
struct sockaddr_un
{
  sa_family_t sun_family;
  char sun_path[UNIX_PATH_MAX];
};
网络字节序和主机字节序
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,
这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这
种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节
序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。
所以: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一
样使用的是Big-Endian。由于 这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以
请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再 赋给socket。参考之前服务器端用来设置监听的IP和端口的
代码:
serv_addr.sin_port = htons(LISTEN_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
这里通过调用两个函数 htons() 和 htolnl() 分别用来将 端口和IP地址转换成网络字节序,这两个函数名中的 h表示host, n表
示network, s表示short(2字节/16位), l表示long(4字节/32位)。因为端口号是16位的,所以我们用htons()把端口号从主机字节
序转换成网络字节序, 而IP地址是32位的,所以我们用htonl()函数把IP地址从主机字节序转换成网络字节序。INADDR_ANY
就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均
定义成为0值。这里也就意味着监听所有的IP地址。
listen()函数
socket()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听
这个socket,该函数将socket变为被动类型的,等待客户的连接请求。
int listen(int sockfd, int backlog);
sockefd: socket()系统调用创建的要监听的socket描述字
backlog: 相应socket可以在内核里排队的最大连接个数
backlog说明:
TCP建立连接是要进行三次握手,但是否完成三次握手后,服务器需要维护这种状态:
  半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态
为:SYN_RCVD)。
  全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一
直保留在半连接状态中;
backlog其实是一个连接队列,在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小,当服务器接收
到客户端的ACK报文后,该条目将从半连接队列搬到全连接队列尾部,即 accept queue (服务器端口状态为:
ESTABLISHED)。在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接
(ESTABLISHED状态)队列大小。
  SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。
  Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为
128。

 

 

accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来
自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返
回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好
了。
 
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
*addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
addrlen: 返回客户端协议地址的长度
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,
则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个
服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户
连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
参考之前的示例代码:
printf("\nStart waiting and accept new client connect...\n", listen_fd);
client_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cliaddr_len);
if(client_fd < 0)
{
  printf("accept new socket failure: %s\n", strerror(errno));
  return -2;
}
printf("Accept new client[%s:%d] with fd [%d]\n", inet_ntoa(cli_addr.sin_addr),
ntohs(cli_addr.sin_port), client_fd);
 
accept()系统调用
将会把客户端的信心保存在cli_addr这个结构体变量中,我们知道cli_addr是struct sockaddr_in 这种IPv4
地址类型,客户端的IP地址和端口号都保存在该结构体中。当然在该结构体中IP地址是以32位整形值的形式存放,端口号也是以
网络字节序的形式存放的。这时我可以使用 inet_ntoa() 函数将32位整形的IP地址转换成点分十进制字符串格式的IP地
址“127.0.0.1”,我们也可以调用ntohs()函数将网络字节序的端口号转换成主机字节序的端口号。
connect()函数
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发
出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接,
通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
addrlen: socket地址的长度
 
新型网路地址转化函数inet_pton和inet_ntop
  在此前的代码中我们使用的inet_ato()或inet_ntoa()函数完成IPv4点分十进制字符串和32位整形数据之间的互相转换,但这两
个函数只适合于IPv4的地址。下面这两个函数可以同时兼容IPv4和IPv6的地址:
 
  //将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
  int inet_pton(int family, const char *strptr, void *addrptr);
 
  /将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL
  const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
 
1. 这两个函数的family参数既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)。如果,以不被支持的地址族作为
family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
2. 第一个函数尝试转换由strptr指针所指向的字符串,并通过addrptr指针存放二进制结果,若成功则返回值为1,否则如果
所指定的family而言输入字符串不是有效的表达式格式,那么返回值为0.
3. inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。inet_ntop函数的strptr参数不可以是一个空
指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。len参数是目标
存储单元的大小,以免该函数溢出其调用者的缓冲区。如果len太小,不足以容纳表达式结果,那么返回一个空指针,并
置为errno为ENOSPC。
read()、write()等函数
客户端在connect()连接服务器,并且服务器通过accept()建立起这个TCP socket链接之后,就可以调用网络I/O函数进行读写
操作了,即实现了网络通信。网络I/O操作函数有下面几组:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t
*addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
 

 

新型网路地址转化函数inet_pton和inet_ntop
在此前的代码中我们使用的inet_ato()或inet_ntoa()函数完成IPv4点分十进制字符串和32位整形数据之间的互相转换,但这两
个函数只适合于IPv4的地址。下面这两个函数可以同时兼容IPv4和IPv6的地址:
//将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1
int inet_pton(int family, const char *strptr, void *addrptr);
 
//将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
1. 这两个函数的family参数既可以是AF_INET(ipv4)也可以是AF_INET6(ipv6)。如果,以不被支持的地址族作为
family参数,这两个函数都返回一个错误,并将errno置为EAFNOSUPPORT.
2. 第一个函数尝试转换由strptr指针所指向的字符串,并通过addrptr指针存放二进制结果,若成功则返回值为1,否则如果
所指定的family而言输入字符串不是有效的表达式格式,那么返回值为0.
3. inet_ntop进行相反的转换,从数值格式(addrptr)转换到表达式(strptr)。inet_ntop函数的strptr参数不可以是一个空
指针。调用者必须为目标存储单元分配内存并指定其大小,调用成功时,这个指针就是该函数的返回值。len参数是目标
存储单元的大小,以免该函数溢出其调用者的缓冲区。如果len太小,不足以容纳表达式结果,那么返回一个空指针,并
置为errno为ENOSPC。
 
 
read()、write()等函数
客户端在connect()连接服务器,并且服务器通过accept()建立起这个TCP socket链接之后,就可以调用网络I/O函数进行读写
操作了,即实现了网络通信。网络I/O操作函数有下面几组:
 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t
*addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

 

  read()是负责从fd中读取内容。当读成功时read返回实际所读的字节数;如果返回的值是0表示已经读到文件的结束了,如果
是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明
读是由中断引起的,如果是ECONNREST表示网络连接出了问题;
  write()将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序
中,当我们向套接字文件描述符写时有俩种可能。
 
1. write的返回值大于0,表示写了部分或者全部的数据。
2. 返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。
如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
 
 
close()、shutdown()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文
件要调用close()来关闭一样。close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述
字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
int close(int fd);
如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候我们需要数据发送出去并到达对方之后才能关闭
socket套接字,则可以调用shutdown()函数来半关闭套接字:
int shutdown(int sockfd, int how);
 
如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如
果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了。

 

 

posted @ 2021-03-03 22:00  一大堆豆子  阅读(326)  评论(0编辑  收藏  举报