Socket编程概念和 Socket之异步TCP客户端断线重连

一:什么是SOCKET

socket的英文原义是“孔”或“插座”。作为进程通信机制,取后一种意思。通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄(其实就是两个程序通信用的)。
socket非常类似于电话插座。以一个电话网为例:电话的通话双方相当于相互通信的2个程序,电话号码就是ip地址。任何用户在通话之前,首先要占有一部电话机,相当于申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,相当于发出连接请求。对方假如在场并空闲,拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤销连接。

Socket编程概念和 Socket之异步TCP客户端断线重连_第1张图片

1、套接字分类

为了满足不同程序对通信质量和性能的要求,一般的网络系统都提供了以下3种不同类型的套接字,以供用户在设计程序时根据不同需要来选择:

流式套接字(SOCK_STREAM):提供了一种可靠的、面向连接的双向数据传输服务。实现了数据无差错,无重复的发送,内设流量控制,被传输的数据被看做无记录边界的字节流。在TCP/IP协议簇中,使用TCP实现字节流的传输,当用户要发送大批量数据,或对数据传输的可靠性有较高要求时使用流式套接字。

数据报套接字(SOCK_DGRAM):提供了一种无连接、不可靠的双向数据传输服务。数据以独立的包形式被发送,并且保留了记录边界,不提供可靠性保证。数据在传输过程中可能会丢失或重复,并且不能保证在接收端数据按发送顺序接收。在TCP/IP协议簇中,使用UDP实现数据报套接字。

原始套接字(SOCK_RAW):该套接字允许对较低层协议(如IP或ICMP)进行直接访问。一般用于对TCP/IP核心协议的网络编程。

Socket编程概念和 Socket之异步TCP客户端断线重连_第2张图片

二:SOCKET相关概念

1、端口

在Internet上有很多这样的主机,这些主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序),因此,在网络协议中使用端口号识别主机上不同的进程
例如:http使用80端口,FTP使用21端口。

可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口,其中虚拟端口指计算 机内部或交换机路由器内的端口,不可见。例如计算机中的80端口、21端口、23端口等。物理端口又称为接口,是可见端口,计算机背板的RJ45网口,交换机路由器集线器等RJ45端口。电话使用RJ11插口也属于物理端口的范畴。
规定一个设备有216个,也就是65536个端口,每个端口对应一个唯一的程序。每个网络程序,无论是客户端还是服务器端,都对应一个或多个特定的端口号。由于0-1024之间多被操作系统占用 ,所以实际编程时一般采用1024以后的端口号
端口详解
端口是指接口电路中的一些寄存器,这些寄存器分别用来存放数据信息、控制信息和状态信息,相应的端口分别称为数据端口、控制端口和状态端口。

电脑运行的系统程序,其实就像一个闭合的圆圈,但是电脑是为人服务的,他需要接受一些指令,并且要按照指令调整系统功能来工作,于是系统程序设计者,就把这个圆圈截成好多段,这些线段接口就叫端口(通俗讲是断口,就是中断),系统运行到这些端口时,一看端口是否打开或关闭,如果关闭,就是绳子接通了,系统往下运行,如果端口是打开的,系统就得到命令,有外部数据输入,接受外部数据并执行.

端口可分为虚拟端口和物理端口
“端口”是英文port的意译,可以认为是设备与外界通讯交流的出口。端口可分为虚拟端口和物理端口,其中虚拟端口指计算机内部或交换机路由器内的端口,不可见。例如计算机中的80端口、21端口、23端口等。物理端口又称为接口,是可见端口,计算机背板的RJ45网口,交换机路由器集线器等RJ45端口。电话使用RJ11插口也属于物理端口的范畴。

硬件端口
CPU通过接口寄存器或特定电路与外设进行数据传送,这些寄存器或特定电路称之为端口。
其中硬件领域的端口又称接口,如:并行端口、串行端口等。

网络端口
在网络技术中,端口(Port)有好几种意思。集线器、交换机、路由器的端口指的是连接其他网络设备的接口,如RJ-45端口、Serial端口等。我们 这里所指的端口不是指物理意义上的端口,而是特指TCP/IP协议中的端口,是逻辑意义上的端口。

软件端口
缓冲区。

TCP端口
TCP[1] :Transmission Control Protocol传输控制协议,TCP是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,由IETF的RFC 793说明(specified)。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能,UDP是同一层内另一个重要的传输协议。

UDP端口
UDP[1] :User Datagram Protocol用户数据报协议,UDP是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP 协议基本上是IP协议与上层协议的接口。UDP协议适用端口分别运行在同一台设备上的多个应用程序。

协议端口

如果把IP地址比作一间房子 ,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口可以有65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(2^16-1)。
在Internet上,各主机间通过TCP/IP协议发送和接收数据包,各个数据包根据其目的主机的ip地址来进行互联网络中的路由选择,把数据包顺利的传送到目的主机。大多数操作系统都支持多程序(进程)同时运行,那么目的主机应该把接收到的数据包传送给众多同时运行的进程中的哪一个呢?显然这个问题有待解决,端口机制便由此被引入进来。
本地操作系统会给那些有需求的进程分配协议端口(protocol port,即我们常说的端口),每个协议端口由一个正整数标识,如:80,139,445,等等。当目的主机接收到数据包后,将根据报文首部的目的端口号,把数据发送到相应端口,而与此端口相对应的那个进程将会领取数据并等待下一组数据的到来。说到这里,端口的概念似乎仍然抽象,那么继续跟我来,别走开。
端口其实就是队,操作系统为各个进程分配了不同的队,数据包按照目的端口被推入相应的队中,等待被进程取用,在极特殊的情况下,这个队也是有可能溢出的,不过操作系统允许各进程指定和调整自己的队的大小。
不光接受数据包的进程需要开启它自己的端口,发送数据包的进程也需要开启端口,这样,数据包中将会标识有源端口,以便接受方能顺利地回传数据包到这个端口。
端口详解
每种网络的服务功能都不相同,因此有必要将不同的封包送给不同的服务来处理,当你的主机同时开启了FTP与WWW服务时,别人送来的资料封包,就会依照 TCP 上面的 port 号码来给 FTP 这个服务或者是 WWW 这个服务来处理。
· 每一个 TCP 连接都必须由一端(通常为 client )发起请求,这个 port 通常是随机选择大于 1024 以上(因为0-1023一般被用作知名服务器的端口,被预定,如FTP、HTTP、SMTP等)的 port 号来进行!其 TCP封包会将(且只将) SYN旗标设定起来!这是整个联机的第一个封包;
· 如果另一端(通常为 Server ) 接受这个请求的话(特殊的服务需要以特殊的 port 来进行,例如 FTP 的 port 21 ),则会向请求端送回整个联机的第二个封包!其上除了 SYN旗标之外同时还将 ACK 旗标也设定起来,并同时在本机端建立资源以待联机之需;
· 然后,请求端获得服务端第一个响应封包之后,必须再响应对方一个确认封包,此时封包只带 ACK旗标(事实上,后继联机中的所有封包都必须带有 ACK 旗标);
· 只有当服务端收到请求端的确认( ACK )封包(也就是整个联机的第三个封包)之后,两端的联机才能正式建立。这就是所谓的 TCP 联机的’三次握手( Three-Way Handshake )’的原理。
经过三向交握之后,你的 client 端的 port 通常是高于 1024 的随机取得的 port,至于主机端则视当时的服务是开启哪一个 port 而定,例如 WWW 选择 80 而 FTP 则以 21 为正常的联机信道!
总而言之,我们这里所说的端口,不是计算机硬件的I/O端口,而是软件形式上的概念。根据提供服务类型的不同,端口分为两种,一种是TCP端口,一种是UDP端口。计算机之间相互通信的时候,分为两种方式:一种是发送信息以后,可以确认信息是否到达,也就是有应答的方式,这种方式大多采用TCP协议;一种是发送以后就不管了,不去确认信息是否到达,这种方式大多采用UDP协议。对应这两种协议的服务提供的端口,也就分为TCP端口和UDP端口。
那么,如果攻击者使用软件扫描目标计算机,得到目标计算机打开的端口,也就了解了目标计算机提供了哪些服务。我们都知道,提供服务就一定有服务软件的漏洞,根据这些,攻击者可以达到对目标计算机的初步了解。如果计算机的端口打开太多,而管理者不知道,那么,有两种情况:一种是提供了服务而管理者没有注意,比如安装IIS的时候,软件就会自动增加很多服务,而管理员可能没有注意到;一种是服务器被攻击者安装木马,通过特殊的端口进行通信。这两种情况都是很危险的,说到底,就是管理员不了解服务器提供的服务,减小了系统安全系数。

端口类型
TCP端口和UDP端口。由于TCP和UDP 两个协议是独立的,因此各自的端口号也相互独立,比如TCP有235端口,UDP也 可以有235端口,两者并不冲突。
1.周知端口(Well Known Ports)
周知端口是众所周知的端口号,范围从0到1023,其中80端口分配给WWW服务,21端口分配给FTP服务等。我们在IE的地址栏里输入一个网址的时候是不必指定端口号的,因为在默认情况下WWW服务的端口是“80”
网络服务是可以使用其他端口号的,如果不是默认的端口号则应该在 地址栏上指定端口号,方法是在地址后面加上冒号“:”(半角),再加上端口号。比如使用“8080”作为WWW服务的端口,则需要在地址栏里输入“网址:8080”。
但是有些系统协议使用固定的端口号,它是不能被改变的,比如139 端口专门用于NetBIOS与TCP/IP之间的通信,不能手动改变。
2.动态端口(Dynamic Ports)
动态端口的范围是从49152到65535。之所以称为动态端口,是因为它 一般不固定分配某种服务,而是动态分配。
3.注册端口
端口1024到49151,分配给用户进程或应用程序。这些进程主要是用户选择安装的一些应用程序,而不是已经分配好了公认端口的常用程序。这些端口在没有被服务器资源占用的时候,可以用用户端动态选用为源端口。

端口作用
我们知道,一台拥有IP地址的主机可以提供许多服务,比如Web服务、FTP服务、SMTP服务等,这些服务完全可以通过1个IP地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠IP地址,因为IP 地址与网络服务的关系是一对多的关系。实际上是通过“IP地址+端口号”来区分不同的服务的。
需要注意的是,端口并不是一一对应的。比如你的电脑作为客户机访 问一台WWW服务器时,WWW服务器使用“80”端口与你的电脑通信,但你的电脑则可能使用“3457”这样的端口。
动态端口(Dynamic Ports)
端口号。
端口在入侵中的作用
有人曾经把服务器比作房子,而把端口比作通向不同房间(服务)的门,如果不考虑细节的话,这是一个不错的比喻。入侵者要占领这间房子,势必要破门而入(物理入侵另说),那么对于入侵者来说,了解房子开了几扇门,都是什么样的门,门后面有什么东西就显得至关重要。
入侵者通常会用扫描器对目标主机的端口进行扫描,以确定哪些端口是开放的,从开放的端口,入侵者可以知道目标主机大致提供了哪些服务,进而猜测可能存在的漏洞,因此对端口的扫描可以帮助我们更好的了解目标主机,而对于管理员,扫描本机的开放端口也是做好安全防范的第一步。

一,认识网卡
       网卡(Network Interface Card,简称NIC),也称网络适配器,是电脑与局域网相互连接的设备。无论是普通电脑还 是高端服务器,只要连接到局域网,就都需要安装一块网卡。如果有必要,一台电脑也可以同时安装两块或多块网卡。
      一块网卡包括OSI 模型的两个层, 物理层和数据链路层:
     1》物理层定义了数据传送与接收所需要的电与光信号、线路状态、时钟基准、数据编码和电路等, 并向数据链路层设备提供标准接口。
     2》数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层 提供标准的数据接口等功能。
二:网卡的组要作用
        网卡的功能主要有两个:
       一是将电脑的数据封装为帧,并通过网线(对无线网络来说就是电磁波)将数据发送到网络上去;
       二是接收网络上其它设备传过来的帧,并将帧重新组合成数据,发送到所在的电脑中。
        网卡能接收所有在网络上传输的 信号,但正常情况下只接受发送到该电脑的帧和广播帧,将其余的帧丢弃。然后,传送到系统CPU 做进一步处理。当电 脑发送数据时,网卡等待合适的时间将分组插入到数据流中。接收系统通知电脑消息是否完整地到达,如果出现问题, 将要求对方重新发送。
————————————————

网络通信三要素:

  • IP地址(网络上主机设备的唯一标识)
  • 端口号(定位程序)
  •      有效端口:0~65535,其中0~1024由系统使用,开发中一般使用1024以上端口.
  • 传输协议(用什么样的方式进行交互)
  •      常见协议:TCP(面向连接,提供可靠的服务),UDP(无连接,传输速度快)

2.IP地址
IP地址是一个规定,现在使用的是IPv4,既由4个0-255之间的数字组成,在计算机内部存储时只需要4个字节即可。在计算机中,IP地址是分配给网卡的,每个网卡有一个唯一的IP地址,如果一个计算机有多个网卡,则该台计算机则拥有多个不同的IP地址,在同一个网络内部,IP地址不能相同。IP地址的概念类似于电话号码、身份证这样的概念。
由于IP地址不方便记忆,所以有专门创造了 域名(Domain Name) 的概念,其实就是给IP取一个字符的名字,例如163.com、sina.com等。IP和域名之间存在一定的对应关系。如果把IP地址类比成身份证号的话,那么域名就是你的姓名。
其实在网络中只能使用IP地址进行数据传输,所以在传输以前,需要把域名转换为IP,这个由称作DNS的服务器专门来完成。
所以在网络编程中,可以使用IP或域名来标识网络上的一台设备。

2、协议

2.1 TCP:

TCP是一种面向连接的、可靠的,基于字节流的传输层通信协议。为两台主机提供高可靠性的数据通信服务。它可以将源主机的数据无差错地传输到目标主机。当有数据要发送时,对应用进程送来的数据进行分片,以适合于在网络层中传输;当接收到网络层传来的分组时,它要对收到的分组进行确认,还要对丢失的分组设置超时重发等。为此TCP需要增加额外的许多开销,以便在数据传输过程中进行一些必要的控制,确保数据的可靠传输。因此,TCP传输的效率比较低。

Socket编程概念和 Socket之异步TCP客户端断线重连_第3张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第4张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第5张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第6张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第7张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第8张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第9张图片

2.1.1 TCP的工作过程

TCP是面向连接的协议,TCP协议通过三个报文段完成类似电话呼叫的连接建立过程,这个过程称为三次握手,如图所示:

Socket编程概念和 Socket之异步TCP客户端断线重连_第10张图片

第一次握手:建立连接时,客户端发送SYN包(SEQ=x)到服务器,并进入SYN_SEND状态,等待服务器确认。

第二次握手:服务器收到SYN包,必须确认客户的SYN(ACK=x+1),同时自己也发送一个SYN包(SEQ=y),即SYN+ACK包,此时服务器进入SYN_RECV状态。

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ACK=y+1),此包发送完毕,客户端和服务器进入Established状态,完成三次握手。

2.1.2 传输数据

一旦通信双方建立了TCP连接,连接中的任何一方都能向对方发送数据和接收对方发来的数据。TCP协议负责把用户数据(字节流)按一定的格式和长度组成多个数据报进行发送,并在接收到数据报之后按分解顺序重新组装和恢复用户数据。
利用TCP传输数据时,数据是以字节流的形式进行传输的。

2.1.3 连接的终止

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如图所示:

Socket编程概念和 Socket之异步TCP客户端断线重连_第11张图片

2.1.4 TCP的主要特点

TCP最主要的特点如下。
(1) 是面向连接的协议。
(2) 端到端的通信。每个TCP连接只能有两个端点,而且只能一对一通信,不能一点对多点直接通信。
(3) 高可靠性。通过TCP连接传送的数据,能保证数据无差错、不丢失、不重复地准确到达接收方,并且保证各数据到达的顺序与其发出的顺序相同。
(4) 全双工方式传输。
(5) 数据以字节流的方式传输。
(6) 传输的数据无消息边界。

Socket编程概念和 Socket之异步TCP客户端断线重连_第12张图片

2.1.5 同步与异步

同步工作方式是指利用TCP编写的程序执行到监听或接收语句时,在未完成工作(侦听到连接请求或收到对方发来的数据)前不再继续往下执行,线程处于阻塞状态,直到该语句完成相应的工作后才继续执行下一条语句。
异步工作方式是指程序执行到监听或接收语句时,不论工作是否完成,都会继续往下执行

2.2 UDP

UDP是一种简单的、面向数据报的无连接的协议,提供的是不一定可靠的传输服务。所谓“无连接”是指在正式通信前不必与对方先建立连接,不管对方状态如何都直接发送过去。这与发手机短信非常相似,只要知道对方的手机号就可以了,不要考虑对方手机处于什么状态。UDP虽然不能保证数据传输的可靠性,但数据传输的效率较高。

2.1.1 UDP与TCP的区别
(1) UDP可靠性不如TCP
TCP包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其他信息,否则将一直等待直到收到确认信息为止。与TCP不同,UDP并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据报的丢失,协议本身并不能做出任何检测或提示。因此,通常人们把UDP称为不可靠的传输协议。
(2) UDP不能保证有序传输
UDP不能确保数据的发送和接收顺序。对于突发性的数据报,有可能会乱序。

2.1.2 UDP的优势

(1) UDP速度比TCP快
由于UDP不需要先与对方建立连接,也不需要传输确认,因此其数据传输速度比TCP快得多。对于强调传输性能而不是传输完整性的应用(比如网络音频播放、视频点播和网络会议等),使用UDP比较合适,因为它的传输速度快,使通过网络播放的视频音质好、画面清晰。
(2) UDP有消息边界
发送方UDP对应用程序交下来的报文,在添加首部后就向下直接交付给IP层。既不拆分,也不合并,而是保留这些报文的边界。使用UDP不需要考虑消息边界问题,这样使得UDP编程相比TCP,在对接收到的数据的处理方面要方便的多。在程序员看来,UDP套接字使用比TCP简单。UDP的这一特征也说明了它是一种面向报文的传输协议。
(3) UDP可以一对多传输
由于传输数据不建立连接,也就不需要维护连接状态(包括收发状态等),因此一台服务器可以同时向多个客户端传输相同的消息。利用UDP可以使用广播或组播的方式同时向子网上的所有客户进程发送消息,这一点也比TCP方便。
其中,速度快是UDP的首要优势
由于TCP协议中植入了各种安全保障功能,在实际执行的过程中会占用大量的系统开销,无疑使速度受到严重影响。反观UDP,由于抛弃了信息可靠传输机制,将安全和排序等功能移交给上层应用完成,极大地降低了执行时间,使速度得到了保证。简而言之,UDP的“理念”就是“不顾一切,只为更快地发送数据”。

Socket编程概念和 Socket之异步TCP客户端断线重连_第13张图片

(4) Socket与TCP/IP协议的关系

Socket是支持TCP/IP协议的网络通信的基本操作单元。数据链路层、网络层、传输层协议是在内核中实现的,因此操作系统需要实现一组系统调用,使得应用程序能够访问这些协议提供的服务,实现这组系统调用的API有socket。Socket是一套通用网络编程接口,它不但可以访问内核中TCP/IP协议栈,而且还可以访问其他网络协议栈(X.25协议栈、UNIX本地域协议栈等)。
socket与TCP/IP协议族的关系:

Socket编程概念和 Socket之异步TCP客户端断线重连_第14张图片

由socket定义的一组API提供两点功能:
一是将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区以交付内核来发送数据,或者是从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据。 

 二是应用程序可以通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而精细地控制底层通信的行为。

Socket编程概念和 Socket之异步TCP客户端断线重连_第15张图片

三:socket一般应用模式:

Socket编程概念和 Socket之异步TCP客户端断线重连_第16张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第17张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第18张图片

 Socket编程概念和 Socket之异步TCP客户端断线重连_第19张图片

端口

服务器端需要一直监听本地端口,看是否有客户端的连接请求,通信过程这里再简要说一下:

  1. 使用socket()创建用于监听的文件描述符listen_fd
  2. 使用bind()将上一步的文件描述符listen_fd与本地的IP和端口绑定
  3. 使用listen()监听listen_fd
  4. 使用accept()建立连接,返回用于通信的文件描述符correspond_fd
  5. 开始收发数据
  6. 关闭socket

在我们用bind绑定端口时,比如http监听80端口,https监听443端口,所以服务端在80端口监听想要建立http连接请求的客户端,在443端口监听https的连接请求。
多个http请求传到80端口,则会进入待连接队列,accept后,则会给各个连接创建进程,并分配一个socket用于通信。
端口号的作用是在网络连接中标识应用层的进程,服务端一般使用众所周知的端口号进行监听,而客户端的端口在连接时由客户端系统自动分配端口号。
一般来说,同一个端口只能被同一种协议下某一个进程监听。
一个端口可以建立多个连接进程,多个不同的套接字可以拥有相同的目的端口号,这是由于各个客户端的IP和端口是不同的。

文件描述符

这里涉及到两个文件描述符:

  • listen_fd
  • correspond_fd

监听的文件描述符listen_fd只需要有一个,它只用于监听连接,而不参与收发数据。
用于通信的文件描述符correspond_fd负责和客户端进行通信,有多少个客户端建立了连接,就有多少个correspond_fd,与之对应的,客户端也有一个correspond_fd负责和服务端通信。
文件描述符对应两块内存结构:一个是读缓冲区,一个是写缓冲区
调用accept时,这个函数查看listen_fd的读缓冲区,如果有数据,则说明有新的客户端连接请求,如果没有数据,则阻塞。
在通信过程中:

  • 发送数据时会调用send/write,这是把数据写入到内核的写缓冲区,若内核检测到该区有数据,则准备发送;
  • 接受数据时会调用recv/read,这是把数据从内核的读缓冲区读出来,若该区没有数据,则会阻塞。

根据socket通信基本流程图,总结通信的基本步骤:
服务器端:
第一步:创建一个用于监听连接的Socket对像;
第二步:用指定的端口号和服务器的ip建立一个EndPoint对像;
第三步:用socket对像的Bind()方法绑定EndPoint;
第四步:用socket对像的Listen()方法开始监听;
第五步:接收到客户端的连接,用socket对像的Accept()方法创建一个新的用于和客户端进行通信的socket对像;
第六步:通信结束后一定记得关闭socket;

客户端:
第一步:建立一个Socket对像;
第二步:用指定的端口号和服务器的ip建立一个EndPoint对像;
第三步:用socket对像的Connect()方法以上面建立的EndPoint对像做为参数,向服务器发出连接请求;
第四步:如果连接成功,就用socket对像的Send()方法向服务器发送信息;
第五步:用socket对像的Receive()方法接受服务器发来的信息 ;
第六步:通信结束后一定记得关闭socket;

Socket编程概念和 Socket之异步TCP客户端断线重连_第20张图片

示例程序:

Socket编程概念和 Socket之异步TCP客户端断线重连_第21张图片

 using System;
  2 using System.Collections.Generic;
  3 using System.ComponentModel;
  4 using System.Data;
  5 using System.Drawing;
  6 using System.Linq;
  7 using System.Net;
  8 using System.Net.Sockets;
  9 using System.Text;
 10 using System.Threading.Tasks;
 11 using System.Windows.Forms;
 12 using System.Threading;
 13 using System.IO;
 14 
 15 namespace SocketServer
 16 {
 17     public partial class FrmServer : Form
 18     {
 19         public FrmServer()
 20         {
 21             InitializeComponent();
 22         }
 23 
 24         //定义回调:解决跨线程访问问题
 25         private delegate void SetTextValueCallBack(string strValue);
 26         //定义接收客户端发送消息的回调
 27         private delegate void ReceiveMsgCallBack(string strReceive);
 28         //声明回调
 29         private SetTextValueCallBack setCallBack;
 30         //声明
 31         private ReceiveMsgCallBack receiveCallBack;
 32         //定义回调:给ComboBox控件添加元素
 33         private delegate void SetCmbCallBack(string strItem);
 34         //声明
 35         private SetCmbCallBack setCmbCallBack;
 36         //定义发送文件的回调
 37         private delegate void SendFileCallBack(byte[] bf);
 38         //声明
 39         private SendFileCallBack sendCallBack;
 40 
 41         //用于通信的Socket
 42         Socket socketSend;
 43         //用于监听的SOCKET
 44         Socket socketWatch;
 45 
 46         //将远程连接的客户端的IP地址和Socket存入集合中
 47         Dictionary dicSocket = new Dictionary();
 48 
 49         //创建监听连接的线程
 50         Thread AcceptSocketThread;
 51         //接收客户端发送消息的线程
 52         Thread threadReceive;
 53 
 54         /// 
 55         /// 开始监听
 56         /// 
 57         /// 
 58         /// 
 59         private void btn_Start_Click(object sender, EventArgs e)
 60         {
 61             //当点击开始监听的时候 在服务器端创建一个负责监听IP地址和端口号的Socket
 62             socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 63             //获取ip地址
 64             IPAddress ip=IPAddress.Parse(this.txt_IP.Text.Trim());
 65             //创建端口号
 66             IPEndPoint point=new IPEndPoint(ip,Convert.ToInt32(this.txt_Port.Text.Trim()));
 67             //绑定IP地址和端口号
 68             socketWatch.Bind(point);
 69             this.txt_Log.AppendText("监听成功"+" \r \n");
 70             //开始监听:设置最大可以同时连接多少个请求
 71             socketWatch.Listen(10);
 72 
 73             //实例化回调
 74             setCallBack = new SetTextValueCallBack(SetTextValue);
 75             receiveCallBack = new ReceiveMsgCallBack(ReceiveMsg);
 76             setCmbCallBack = new SetCmbCallBack(AddCmbItem);
 77             sendCallBack = new SendFileCallBack(SendFile);
 78 
 79             //创建线程
 80             AcceptSocketThread = new Thread(new ParameterizedThreadStart(StartListen));
 81             AcceptSocketThread.IsBackground = true;
 82             AcceptSocketThread.Start(socketWatch);
 83         }
 84 
 85         /// 
 86         /// 等待客户端的连接,并且创建与之通信用的Socket
 87         /// 
 88         /// 
 89         private void StartListen(object obj)
 90         {
 91             Socket socketWatch = obj as Socket;
 92             while (true)
 93             {               
 94                 //等待客户端的连接,并且创建一个用于通信的Socket
 95                 socketSend = socketWatch.Accept();
 96                 //获取远程主机的ip地址和端口号
 97                 string strIp=socketSend.RemoteEndPoint.ToString();
 98                 dicSocket.Add(strIp, socketSend);
 99                 this.cmb_Socket.Invoke(setCmbCallBack, strIp);
100                 string strMsg = "远程主机:" + socketSend.RemoteEndPoint + "连接成功";
101                 //使用回调
102                 txt_Log.Invoke(setCallBack, strMsg);
103 
104                 //定义接收客户端消息的线程
105                 Thread threadReceive = new Thread(new ParameterizedThreadStart(Receive));
106                 threadReceive.IsBackground = true;
107                 threadReceive.Start(socketSend);
108 
109             }
110         }
111 
112        
113 
114         /// 
115         /// 服务器端不停的接收客户端发送的消息
116         /// 
117         /// 
118         private void Receive(object obj)
119         {
120             Socket socketSend = obj as Socket;
121             while (true)
122             {
123                 //客户端连接成功后,服务器接收客户端发送的消息
124                 byte[] buffer = new byte[2048];
125                 //实际接收到的有效字节数
126                 int count = socketSend.Receive(buffer);
127                 if (count == 0)//count 表示客户端关闭,要退出循环
128                 {
129                     break;
130                 }
131                 else
132                 {
133                     string str = Encoding.Default.GetString(buffer, 0, count);
134                     string strReceiveMsg = "接收:" + socketSend.RemoteEndPoint + "发送的消息:" + str;
135                     txt_Log.Invoke(receiveCallBack, strReceiveMsg);
136                 }
137             }
138         }
139 
140         /// 
141         /// 回调委托需要执行的方法
142         /// 
143         /// 
144         private void SetTextValue(string strValue)
145         {
146             this.txt_Log.AppendText(strValue + " \r \n");
147         }
148 
149 
150         private void ReceiveMsg(string strMsg)
151         {
152             this.txt_Log.AppendText(strMsg + " \r \n");
153         }
154 
155         private void AddCmbItem(string strItem)
156         {
157             this.cmb_Socket.Items.Add(strItem);
158         }
159 
160         /// 
161         /// 服务器给客户端发送消息
162         /// 
163         /// 
164         /// 
165         private void btn_Send_Click(object sender, EventArgs e)
166         {
167             try
168             {
169                 string strMsg = this.txt_Msg.Text.Trim();
170                 byte[] buffer = Encoding.Default.GetBytes(strMsg);
171                 List list = new List();
172                 list.Add(0);
173                 list.AddRange(buffer);
174                 //将泛型集合转换为数组
175                 byte[] newBuffer = list.ToArray();
176                 //获得用户选择的IP地址
177                 string ip = this.cmb_Socket.SelectedItem.ToString();
178                 dicSocket[ip].Send(newBuffer);
179             }
180             catch (Exception ex)
181             {
182                 MessageBox.Show("给客户端发送消息出错:"+ex.Message);
183             }
184             //socketSend.Send(buffer);
185         }
186 
187         /// 
188         /// 选择要发送的文件
189         /// 
190         /// 
191         /// 
192         private void btn_Select_Click(object sender, EventArgs e)
193         {
194             OpenFileDialog dia = new OpenFileDialog();
195             //设置初始目录
196             dia.InitialDirectory = @"";
197             dia.Title = "请选择要发送的文件";
198             //过滤文件类型
199             dia.Filter = "所有文件|*.*";
200             dia.ShowDialog();
201             //将选择的文件的全路径赋值给文本框
202             this.txt_FilePath.Text = dia.FileName;
203         }
204 
205         /// 
206         /// 发送文件
207         /// 
208         /// 
209         /// 
210         private void btn_SendFile_Click(object sender, EventArgs e)
211         {
212             List list = new List();
213             //获取要发送的文件的路径
214             string strPath = this.txt_FilePath.Text.Trim();
215             using (FileStream sw = new FileStream(strPath,FileMode.Open,FileAccess.Read))
216             {
217                 byte[] buffer = new byte[2048];
218                 int r = sw.Read(buffer, 0, buffer.Length);
219                 list.Add(1);
220                 list.AddRange(buffer);
221 
222                 byte[] newBuffer = list.ToArray();
223                 //发送
224                 //dicSocket[cmb_Socket.SelectedItem.ToString()].Send(newBuffer, 0, r+1, SocketFlags.None);
225                 btn_SendFile.Invoke(sendCallBack, newBuffer);
226 
227                 
228             }
229             
230         }
231 
232         private void SendFile(byte[] sendBuffer)
233         {
234 
235             try
236             {
237                 dicSocket[cmb_Socket.SelectedItem.ToString()].Send(sendBuffer, SocketFlags.None);
238             }
239             catch (Exception ex)
240             {
241                 MessageBox.Show("发送文件出错:"+ex.Message);
242             }
243         }
244 
245         private void btn_Shock_Click(object sender, EventArgs e)
246         {
247             byte[] buffer = new byte[1] { 2};
248             dicSocket[cmb_Socket.SelectedItem.ToString()].Send(buffer);
249         }
250 
251         /// 
252         /// 停止监听
253         /// 
254         /// 
255         /// 
256         private void btn_StopListen_Click(object sender, EventArgs e)
257         {
258             socketWatch.Close();
259             socketSend.Close();
260             //终止线程
261             AcceptSocketThread.Abort();
262             threadReceive.Abort();
263         }
264     }
265 }

Socket编程概念和 Socket之异步TCP客户端断线重连_第22张图片

1 using System;
  2 using System.Collections.Generic;
  3 using System.ComponentModel;
  4 using System.Data;
  5 using System.Drawing;
  6 using System.Linq;
  7 using System.Text;
  8 using System.Threading.Tasks;
  9 using System.Windows.Forms;
 10 using System.Net.Sockets;
 11 using System.Net;
 12 using System.Threading;
 13 using System.IO;
 14 
 15 namespace SocketClient
 16 {
 17     public partial class FrmClient : Form
 18     {
 19         public FrmClient()
 20         {
 21             InitializeComponent();
 22         }
 23 
 24         //定义回调
 25         private delegate void SetTextCallBack(string strValue);
 26         //声明
 27         private SetTextCallBack setCallBack;
 28 
 29         //定义接收服务端发送消息的回调
 30         private delegate void ReceiveMsgCallBack(string strMsg);
 31         //声明
 32         private ReceiveMsgCallBack receiveCallBack;
 33 
 34         //创建连接的Socket
 35         Socket socketSend;
 36         //创建接收客户端发送消息的线程
 37         Thread threadReceive;
 38 
 39         /// 
 40         /// 连接
 41         /// 
 42         /// 
 43         /// 
 44         private void btn_Connect_Click(object sender, EventArgs e)
 45         {
 46             try
 47             {
 48                 socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
 49                 IPAddress ip = IPAddress.Parse(this.txt_IP.Text.Trim());
 50                 socketSend.Connect(ip, Convert.ToInt32(this.txt_Port.Text.Trim()));
 51                 //实例化回调
 52                 setCallBack = new SetTextCallBack(SetValue);
 53                 receiveCallBack = new ReceiveMsgCallBack(SetValue);
 54                 this.txt_Log.Invoke(setCallBack, "连接成功");
 55 
 56                 //开启一个新的线程不停的接收服务器发送消息的线程
 57                 threadReceive = new Thread(new ThreadStart(Receive));
 58                 //设置为后台线程
 59                 threadReceive.IsBackground = true;
 60                 threadReceive.Start();
 61             }
 62             catch (Exception ex)
 63             {
 64                 MessageBox.Show("连接服务端出错:" + ex.ToString());
 65             }
 66         }
 67 
 68         /// 
 69         /// 接口服务器发送的消息
 70         /// 
 71         private void Receive()
 72         {
 73             try
 74             {
 75                 while (true)
 76                 {
 77                     byte[] buffer = new byte[2048];
 78                     //实际接收到的字节数
 79                     int r = socketSend.Receive(buffer);
 80                     if (r == 0)
 81                     {
 82                         break;
 83                     }
 84                     else
 85                     {
 86                         //判断发送的数据的类型
 87                         if (buffer[0] == 0)//表示发送的是文字消息
 88                         {
 89                             string str = Encoding.Default.GetString(buffer, 1, r - 1);
 90                             this.txt_Log.Invoke(receiveCallBack, "接收远程服务器:" + socketSend.RemoteEndPoint + "发送的消息:" + str);
 91                         }
 92                         //表示发送的是文件
 93                         if (buffer[0] == 1)
 94                         {
 95                             SaveFileDialog sfd = new SaveFileDialog();
 96                             sfd.InitialDirectory = @"";
 97                             sfd.Title = "请选择要保存的文件";
 98                             sfd.Filter = "所有文件|*.*";
 99                             sfd.ShowDialog(this);
100 
101                             string strPath = sfd.FileName;
102                             using (FileStream fsWrite = new FileStream(strPath, FileMode.OpenOrCreate, FileAccess.Write))
103                             {
104                                 fsWrite.Write(buffer, 1, r - 1);
105                             }
106 
107                             MessageBox.Show("保存文件成功");
108                         }
109                     }
110 
111 
112                 }
113             }
114             catch (Exception ex)
115             {
116                 MessageBox.Show("接收服务端发送的消息出错:" + ex.ToString());
117             }
118         }
119 
120 
121         private void SetValue(string strValue)
122         {
123             this.txt_Log.AppendText(strValue + "\r \n");
124         }
125 
126         /// 
127         /// 客户端给服务器发送消息
128         /// 
129         /// 
130         /// 
131         private void btn_Send_Click(object sender, EventArgs e)
132         {
133             try
134             {
135                 string strMsg = this.txt_Msg.Text.Trim();
136                 byte[] buffer = new byte[2048];
137                 buffer = Encoding.Default.GetBytes(strMsg);
138                 int receive = socketSend.Send(buffer);
139             }
140             catch (Exception ex)
141             {
142                 MessageBox.Show("发送消息出错:" + ex.Message);
143             }
144         }
145 
146         private void FrmClient_Load(object sender, EventArgs e)
147         {
148             Control.CheckForIllegalCrossThreadCalls = false;
149         }
150 
151         /// 
152         /// 断开连接
153         /// 
154         /// 
155         /// 
156         private void btn_CloseConnect_Click(object sender, EventArgs e)
157         {
158             //关闭socket
159             socketSend.Close();
160             //终止线程
161             threadReceive.Abort();
162         }
163     }
164 }

代码测试:

Socket编程概念和 Socket之异步TCP客户端断线重连_第23张图片

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Socket
{
    class ClientSocket
    {
        public class SocketClientManager
        {
            public delegate int ConnectStateEventHandler(int a, int b);
            public event ConnectStateEventHandler ConnectedEvent;//连接成功
            public event ConnectStateEventHandler DisConnectedEvent;//连接失败

            public delegate void ReceiveMsgEventHandler(byte[] order);
            public event ReceiveMsgEventHandler ReceiveMsgEvent;

            static System.Net.Sockets.Socket _socket = null;
            static IPEndPoint iep = null;
            static bool isConnecting = false;
            static bool isConnected = false;

            public SocketClientManager(string strIP, int port)
            {
                _socket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress IP = IPAddress.Parse(strIP.Trim());
                iep = new IPEndPoint(IP, port);
            }

            public void Start()
            {
                isConnecting = true;
                ConnectedEvent += send1;
                DisConnectedEvent += send1;
                int aa = DisConnectedEvent(1, 2);
                Thread t = new Thread(Connect);
                t.IsBackground = true;
                t.Start();
            }

            private void Connect()
            {
                while (isConnecting)
                {
                    try
                    {
                        if (!IsSocketConnected(_socket) && DisConnectedEvent != null)
                        {

                        }
                    }
                    catch { }
                    finally
                    {
                        Thread.Sleep(500);
                    }
                }
            }

            private void ReceiveMsg()
            {
                try
                {
                    while (isConnected)
                    {
                        byte[] buffer = new byte[32];
                        int count = _socket.Receive(buffer);
                        if (count > 0 && ReceiveMsgEvent != null)
                        {
                            ReceiveMsgEvent(buffer);
                        }
                    }
                }
                catch
                {

                }
            }

            public void SendMsg(byte[] order)
            {
                _socket.Send(order, order.Length, SocketFlags.None);
            }

            private bool IsSocketConnected(System.Net.Sockets.Socket s)
            {
               
            }
            private static int send1(int a, int b)
            {
                int aa = 0;
                return aa;
            }
        }

    }
}

Socket编程概念和 Socket之异步TCP客户端断线重连_第24张图片

 Socket编程概念和 Socket之异步TCP客户端断线重连_第25张图片

AF 定义: 地址族 - Address Family
WSA  windows socket async
内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace THLaserSoftware.OSocket
{
    //异步Socket
    public class SocketClientAsync
    {
        #region 声明变量
        public string IPAdress;
        public bool connected = false;
        public Socket clientSocket;
        private IPEndPoint hostEndPoint;
        private int Flag = 0;
        private AutoResetEvent autoConnectEvent = new AutoResetEvent(false);
        private SocketAsyncEventArgs lisnterSocketAsyncEventArgs;

        public delegate void StartListeHandler();
        public event StartListeHandler StartListen;

        public delegate void ReceiveMsgHandler(byte[] info, int i);
        public event ReceiveMsgHandler OnMsgReceived;

        private List s_lst = new List();
        static byte[] buffer = new byte[1024 * 1024];
        #endregion
        #region 构造函数
        /// 
        /// 构造函数
        /// 
        /// 
        /// 
        /// 
        public OSocketClientAsync(string hostName, int port, int i)
        {
            Flag = i;
            IPAdress = hostName;
            IPAddress[] hostAddresses = Dns.GetHostAddresses(hostName);
            this.hostEndPoint = new IPEndPoint(hostAddresses[hostAddresses.Length - 1], port);
            this.clientSocket = new Socket(this.hostEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        }
        #endregion
        #region 开始连接服务端
        /// 
        /// 连接服务端
        /// 
        /// 
        private bool Connect()
        {
            using (SocketAsyncEventArgs args = new SocketAsyncEventArgs())
            {
                args.UserToken = this.clientSocket;
                args.RemoteEndPoint = this.hostEndPoint;
                args.Completed += new EventHandler(this.OnConnect);
                this.clientSocket.ConnectAsync(args);
                bool flag = autoConnectEvent.WaitOne(5000);
                if (this.connected)
                {
                    this.lisnterSocketAsyncEventArgs = new SocketAsyncEventArgs();
                    byte[] buffer = new byte[1024];
                    this.lisnterSocketAsyncEventArgs.UserToken = this.clientSocket;
                    this.lisnterSocketAsyncEventArgs.SetBuffer(buffer, 0, buffer.Length);
                    this.lisnterSocketAsyncEventArgs.Completed += new EventHandler(this.OnReceive);
                    this.StartListen();
                    return true;
                }
                return false;
            }
        }
        #endregion
        #region 判断有没有连接上服务端
        /// 
        /// 判断有没有连接上
        /// 
        /// 
        /// 
        private void OnConnect(object sender, SocketAsyncEventArgs e)
        {
            this.connected = (e.SocketError == SocketError.Success);
            autoConnectEvent.Set();
        }
        #endregion
        #region 发送数据到服务端
        /// 
        /// 发送
        /// 
        /// 
        public void Send(byte[] mes)
        {
            if (this.connected)
            {
                EventHandler handler = null;
                //byte[] buffer = Encoding.Default.GetBytes(mes);
                buffer = new byte[1024 * 1024];
                for (int i = 0; i < buffer.Length; i++)
                {
                    buffer[i] = 0x00;
                }
                Array.Copy(mes, 0, buffer, 0, mes.Length);
                SocketAsyncEventArgs senderSocketAsyncEventArgs = null;
                lock (s_lst)
                {
                    if (s_lst.Count > 0)
                    {
                        senderSocketAsyncEventArgs = s_lst[s_lst.Count - 1];
                        s_lst.RemoveAt(s_lst.Count - 1);
                    }
                }
                if (senderSocketAsyncEventArgs == null)
                {
                    senderSocketAsyncEventArgs = new SocketAsyncEventArgs();
                    senderSocketAsyncEventArgs.UserToken = this.clientSocket;
                    senderSocketAsyncEventArgs.RemoteEndPoint = this.clientSocket.RemoteEndPoint;
                    if (handler == null)
                    {
                        handler = delegate (object sender, SocketAsyncEventArgs _e)
                        {
                            lock (s_lst)
                            {
                                s_lst.Add(senderSocketAsyncEventArgs);
                            }
                        };
                    }
                    senderSocketAsyncEventArgs.Completed += handler;
                }
                senderSocketAsyncEventArgs.SetBuffer(buffer, 0, buffer.Length);
                this.clientSocket.SendAsync(senderSocketAsyncEventArgs);
            }
            else
            {
                this.connected = false;
            }
        }
        #endregion
        #region 监听服务端
        /// 
        /// 监听服务端
        /// 
        public void Listen()
        {
            if (this.connected && this.clientSocket != null)
            {
                try
                {
                    (lisnterSocketAsyncEventArgs.UserToken as Socket).ReceiveAsync(lisnterSocketAsyncEventArgs);
                }
                catch (Exception)
                {
                }
            }
        }
        #endregion
        #region 断开服务端的连接
        /// 
        /// 断开连接
        /// 
        /// 
        private int Disconnect()
        {
            int res = 0;
            try
            {
                this.clientSocket.Shutdown(SocketShutdown.Both);
            }
            catch (Exception)
            {
            }
            try
            {
                this.clientSocket.Close();
            }
            catch (Exception)
            {
            }
            this.connected = false;
            return res;
        }
        #endregion
        #region 数据接收
        /// 
        /// 数据接受
        /// 
        /// 
        /// 
        private void OnReceive(object sender, SocketAsyncEventArgs e)
        {
            if (e.BytesTransferred == 0)
            {
                if (clientSocket.Connected)
                {
                    try
                    {
                        this.clientSocket.Shutdown(SocketShutdown.Both);
                    }
                    catch (Exception)
                    {
                    }
                    finally
                    {
                        if (this.clientSocket.Connected)
                        {
                            this.clientSocket.Close();
                        }
                    }
                }
                byte[] info = new Byte[] { 0 };
                this.OnMsgReceived(info, Flag);
            }
            else
            {
                byte[] buffer = new byte[e.BytesTransferred];
                Buffer.BlockCopy(e.Buffer, 0, buffer, 0, e.BytesTransferred);
                this.OnMsgReceived(buffer, Flag);
                Listen();
            }
        }

        #endregion
        #region 建立连接服务端的方法
        /// 
        /// 建立连接的方法
        /// 
        /// 
        public bool ConnectServer()
        {
            bool flag = false;
            this.StartListen += new StartListeHandler(SocketClient_StartListen);
            // this.OnMsgReceived += new ReceiveMsgHandler(SocketClient_OnMsgReceived);
            flag = this.Connect();
            if (!flag)
            {
                return flag;
            }
            return true;
        }
        #endregion
        #region 关闭与服务端之间的连接
        /// 
        /// 关闭连接的方法
        /// 
        /// 
        public int CloseLinkServer()
        {
            return this.Disconnect();
        }
        #endregion
        #region 监听方法
        /// 
        /// 监听的方法
        /// 
        private void SocketClient_StartListen()
        {
            this.Listen();
        }
        #endregion
        #region IDispose member
        public void Dispose()
        {
            if (this.clientSocket.Connected)
            {
                this.clientSocket.Close();
            }
        }
        #endregion
        #region 析构函数
        ~OSocketClientAsync()
        {
            try
            {
                if (this.clientSocket.Connected)
                {
                    this.clientSocket.Close();
                }
            }
            catch
            {

            }
            finally
            {

            }
        }
        #endregion
    }


}

Linux 进程、线程、文件描述符的底层原理

说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:在 Linux 系统中,进程和线程几乎没有区别。Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度看看为什么说线程和进程基本没有区别。

一、进程是什么

首先,抽象地来说,我们的计算机就是这个东西:Socket编程概念和 Socket之异步TCP客户端断线重连_第26张图片

这个大的矩形表示计算机的内存空间,其中的小矩形代表进程,左下角的圆形表示磁盘,右下角的图形表示一些输入输出设备,比如鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示用户空间,下半部分表示内核空间
用户空间装着用户进程需要使用的资源,比如你在程序代码里开一个数组,这个数组肯定存在用户空间;内核空间存放内核进程需要加载的系统资源,这一些资源一般是不允许用户访问的。但是注意有的用户进程会共享一些内核空间的资源,比如一些动态链接库等等。
我们用 C 语言写一个 hello 程序,编译后得到一个可执行文件,在命令行运行就可以打印出一句 hello world,然后程序退出。在操作系统层面,就是新建了一个进程,这个进程将我们编译出来的可执行文件读入内存空间,然后执行,最后退出。

你编译好的那个可执行程序只是一个文件,不是进程,可执行文件必须要载入内存,包装成一个进程才能真正跑起来。进程是要依靠操作系统创建的,每个进程都有它的固有属性,比如进程号(PID)、进程状态、打开的文件等等,进程创建好之后,读入你的程序,你的程序才被系统执行。
那么,操作系统是如何创建进程的呢?对于操作系统,进程就是一个数据结构,我们直接来看 Linux 的源码:

struct task_struct {
    // 进程状态
    long              state;
    // 虚拟内存结构体
    struct mm_struct  *mm;
    // 进程号
    pid_t             pid;
    // 指向父进程的指针
    struct task_struct __rcu  *parent;
    // 子进程列表
    struct list_head        children;
    // 存放文件系统信息的指针
    struct fs_struct        *fs;
    // 一个数组,包含该进程打开的文件指针
    struct files_struct     *files;
};

task_struct就是 Linux 内核对于一个进程的描述,也可以称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。其中比较有意思的是mm指针和files指针。mm指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;files指针指向一个数组,这个数组里装着所有该进程打开的文件的指针

二、文件描述符是什么

先说files,它是一个文件指针数组。一般来说,一个进程会从files[0]读取输入,将输出写入files[1],将错误信息写入files[2]
有个小疑问 files指针不是指向一个结构体吗?
为啥答主说“files指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。”
我理解:应该说文件(file)指针指向一个结构体,这个结构体的指针放在files数组中

举个例子,以我们的角度 C 语言的printf函数是向命令行打印字符,但是从进程的角度来看,就是向files[1]写入数据;同理,scanf函数就是进程试图从files[0]这个文件中读取数据。
每个进程被创建时,files的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。
我们可以重新画一幅图:

Socket编程概念和 Socket之异步TCP客户端断线重连_第27张图片

 明白了这个原理,输入重定向就很好理解了,程序想读取数据的时候就会去files[0]读取,所以我们只要把files[0]指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

$ command < file.txt

Socket编程概念和 Socket之异步TCP客户端断线重连_第28张图片

 错误重定向也是一样的,就不再赘述。
管道符其实也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美:

$ cmd1 | cmd2 | cmd3

Socket编程概念和 Socket之异步TCP客户端断线重连_第29张图片

 到这里,你可能也看出「Linux 中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket 套接字还是真正的文件,全部都可以读写,统一装进一个简单的files数组,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美高效。

三、线程是什么

首先要明确的是,多进程和多线程都是并发,都可以提高处理器的利用效率,所以现在的关键是,多线程和多进程有啥区别。
为什么说 Linux 中线程和进程基本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和进程区别对待。
我们知道系统调用fork()可以新建一个子进程,函数pthread()可以新建一个线程。但无论线程还是进程,都是用task_struct结构表示的,唯一的区别就是共享的数据区域不同。换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就比如说,mm结构和files结构在线程中都是共享的,我画两张图你就明白了:Socket编程概念和 Socket之异步TCP客户端断线重连_第30张图片

Socket编程概念和 Socket之异步TCP客户端断线重连_第31张图片

 所以说,我们的多线程程序要利用锁机制,避免多个线程同时往同一区域写入数据,否则可能造成数据错乱。
那么你可能问,既然进程和线程差不多,而且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使用比多进程普遍得多呢
因为现实中数据共享的并发更普遍呀,比如十个人同时从一个账户取十元,我们希望的是这个共享账户的余额正确减少一百元,而不是希望每人获得一个账户的拷贝,每个拷贝账户减少十元。
当然,必须要说明的是,只有 Linux 系统将线程看做共享数据的进程,不对其做特殊看待,其他的很多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个人认为不如 Linux 的这种设计简洁,增加了系统的复杂度。
在 Linux 中新建线程和进程的效率都是很高的,对于新建进程时内存区域拷贝的问题,Linux 采用了 copy-on-write 的策略优化,也就是并不真正复制父进程的内存空间,而是等到需要写操作时才去复制。所以 Linux 中新建进程和新建线程都是很迅速的

Linux文件描述符到底是什么?

Linux 中一切都可以看作文件,包括普通文件、链接文件、Socket 以及设备驱动等,对其进行相关操作时,都可能会创建对应的文件描述符。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,用于指代被打开的文件,对文件所有 I/O 操作相关的系统调用都需要通过文件描述符。Linux 中一切皆文件,比如 C++ 源文件、视频文件、Shell脚本、可执行文件等,就连键盘、显示器、鼠标等硬件设备也都是文件。
一个 Linux 进程可以打开成百上千个文件,为了表示和区分已经打开的文件,Linux 会给每个文件分配一个编号(一个 ID),这个编号就是一个整数,被称为文件描述符(File Descriptor)。
这只是一个形象的比喻,为了让读者容易理解我才这么说。如果你也仅仅理解到这个层面,那不过是浅尝辄止而已,并没有看到文件描述符的本质。
本篇文章的目的就是拨云见雾,从底层实现的角度来给大家剖析一下文件描述符,看看文件描述如到底是如何表示一个文件的。
不过,阅读本篇文章需要你有C语言编程基础,至少要理解数组、指针和结构体;如果理解内存,那就更好了,看了这篇文章你会醍醐灌顶。

Linux 文件描述符到底是什么?一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。

为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块(PCB Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。

内核空间是虚拟地址空间的一部分,想死磕的读者请猛击《 C语言内存精讲》,不想纠缠细节的读者可以这样理解:进程启动后要占用内存,其中一部分内存分配给了文件描述符表。

除了文件描述符表,系统还需要维护另外两张表:

  • 打开文件表(Open file table)
  • i-node 表(i-node table)

文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示。
 

Socket编程概念和 Socket之异步TCP客户端断线重连_第32张图片

  • 进程级别的文件描述符表:内核为每个进程维护一个文件描述符表,该表记录了文件描述符的相关信息,包括文件描述符、指向打开文件表中记录的指针。
  • 系统级别的打开文件表:内核对所有打开文件维护的一个进程共享的打开文件描述表,表中存储了处于打开状态文件的相关信息,包括文件类型、访问权限、文件操作函数(file_operations)等。
  • 系统级别的 i-node 表:i-node 结构体记录了文件相关的信息,包括文件长度,文件所在设备,文件物理位置,创建、修改和更新时间等,"ls -i" 命令可以查看文件 i-node 节点

从本质上讲,这三种表都是结构体数组,0、1、2、73、1976 等都是数组下标。表头只是我自己添加的注释,数组本身是没有的。实线箭头表示指针的指向,虚线箭头是我自己添加的注释。

你看,文件描述符只不过是一个数组下标吗!

通过文件描述符,可以找到文件指针,从而进入打开文件表。该表存储了以下信息:

  • 文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。
  • 状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。
  • i-node 表指针。

然而,要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表,该表包含了诸如以下的信息:

  • 文件类型,例如常规文件、套接字或 FIFO。
  • 文件大小。
  • 时间戳,比如创建时间、更新时间。
  • 文件锁。

对上图的进一步说明:

  • 在进程 A 中,文件描述符 1 和 20 都指向了同一个打开文件表项,标号为 23(指向了打开文件表中下标为 23 的数组元素),这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。
  • 进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件,这可能是在调用 fork() 后出现的(即进程 A、B 是父子进程关系),或者是不同的进程独自去调用 open() 函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
  • 进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项,但这些表项均指向 i-node 表的同一个条目(标号为 1976);换言之,它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。 

有了以上对文件描述符的认知,我们很容易理解以下情形:

  • 同一个进程的不同文件描述符可以指向同一个文件;
  • 不同进程可以拥有相同的文件描述符;
  • 不同进程的同一个文件描述符可以指向不同的文件(一般也是这样,除了 0、1、2 这三个特殊的文件);
  • 不同进程的不同文件描述符也可以指向同一个文件。

socket缓冲区以及阻塞模式详解

在《socket 数据的接受和发送》一节中讲到,可以使用 write()/send() 函数发送数据,使用 read()/recv() 函数接收数据,本节就来看看数据是如何传递的。

socket 缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。
TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。
read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

Socket编程概念和 Socket之异步TCP客户端断线重连_第33张图片

 图:TCP套接字的I/O缓冲区示意图
这些I/O缓冲区特性可整理如下:

  • I/O缓冲区在每个TCP套接字中单独存在;
  • I/O缓冲区在创建套接字时自动生成;
  • 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
  • 关闭套接字将丢失输入缓冲区中的数据。

输入输出缓冲区的默认大小一般都是 8K,可以通过 getsockopt() 函数获取:

unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal);

运行结果:
Buffer length: 8192

阻塞模式

对于TCP套接字(默认情况下),当使用 write()/send() 发送数据时:
1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。
4) 直到所有数据被写入缓冲区 write()/send() 才能返回。

当使用 read()/recv() 读取数据时:
1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。
2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
3) 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。
这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

TCP套接字默认情况下是阻塞模式,也是最常用的。当然也可以更改为非阻塞模式

你可能感兴趣的:(网络,c#,socket,tcpip)