一.网络编程的预备知识
1.网络通信
通信都应该具备两个条件:
(1)物理媒介(物理层面)
比如:土电话,座机等等....
(2)协议(软件层次)
网络协议
2.网络协议层次模型
互联网的本质就是一系列的网络协议,处于互联网上的两台计算机如果要实现通信,就需要遵守这些协议,比如:网线的接口类型,
寻址方式,数据如何发送等一系列协议。
简单的说,协议就是计算机之间通过网络实现通信事先达成的一种"约定";这种约定使不同的厂商的设备,不同cpu以及不同操作
系统组成的计算机,只要遵守相同协议就可以通信。
协议可以分成很多种,每一种协议都明确界定了它的行为规范:2台计算机之间支持相同的协议,并且遵循相同协议进行处理,才能
实现通信。
那么这些协议根据功能的不同和分工不同划分了不同的层次,常见的有2种:
OSI七层模型 TCP/IP 四层模型
--------------------------------------------------------------------------
应用层
表示层 ----> 应用层
会话层
传输层 ----> 传输层
网络层 ----> 网络层
数据链路层 ----> 网络接口层
物理层
OSI七层模型只是理论上的,现代网络通信更多的是利用四层模型。
发送方从最高层开始,从上到下按顺序传输数据,每一层接收到上层处理的数据时,添加该层的首部并可能对数据进行处理,接收
放则将顺序反过来,从首层开始,将数据的内容与该层对应的首部拆开,传给上一层。
说白了,数据在发送的时候是数据从应用层到物理层的一个打包过程,接收时数据从物理层到应用层的一个解包的过程。
那么各层的大体的功能和作用如下:
(1)应用层
应用层是直接面向用户的程序或者服务。比如我们发送数据是先由用户进程传递给应用层,而接收数据也是由应用层
直接呈现给用户进程
应用层要完成用户希望在网络上完成各种工作。因为用户可能对发送的数据有各种的要求。
这一层可以想象成快递公司的收件员,当客户(应用进程)打电话(发送请求)给收件员(应用层)时,收件员可以根据用户的
不同的需求提供不同的服务(不同的协议),比如发送的时间......等等
工作于这一层的协议接触的比较多的主要就有HTTP,FTP等
(2)表示层
表示层是为了异种机通信提供的一种公共语言,以便于进行操作,常见的协议有ASCII,SSL/TLS等
什么叫异种机?就比如我要用linux给window发数据,两个操作系统的语法是不一致,就像安装包,exe是不能在linux下使用
shell在window下也是不能运行的,于是就需要表示层,帮助我们解决不同操作系统之间的通信语法问题。
就像是通信的双方,一个是中国人,一个是韩国人,双方是无法直接交流的,那么发送方(中国)的表示层可能会把中文翻译
成英语,接收方(韩国)的表示层就会把英语翻译成韩文,这样就能正常交流。
再比如就像浏览器请求回了一堆数据,是解析成文本还是图片,就是由表示层决定。而且数据的压缩,加密,打包等功能也是
这层完成
(3)会话层
会话层的作用就是建立或者断开和管库应用程序之间的通信
会话层可以把其堪称快递公司的调度员。它管理着快递的相关信息,就比如快递何时发送过去,需要多少时间...
它也要做同步管理,就好比我把东西运过去了,但是那边没人接收,这种情况就是没有同步
(4)传输层
传输层起着可靠传输的作用,确保数据被可靠的传送到目标地址。
此层具有两个代表性的协议:TCP UDP
4.1 TCP: Transport Control Protocol 传输层控制协议
它是一种面向连接的传输层协议,它能提供可靠的通信
就是说TCP协议是在确定对方能通信的前提下才会传输数据,如果对方不能通信,那么发送方一直再发送请求包,直到对方
回应,双方建立连接,开始传输数据。
但是传输的过程中,有可能数据会丢失,一旦丢失,TCP也会继续重发。就比如我发了1000个包,另外一台电脑就要告诉我
是否接收了这1000个包,如果丢了一些包,你也要告诉我是丢了哪些包,我会把丢的包重新发送一遍,这样的数据的完整率就会
大大提高。
所以,TCP协议是数据无误,数据无丢失,数据无失序,数据无重复到达的通信协议,因此TCP协议一般用于发送大量的比较
重要的数据。
4.2 UDP: User Datagram Protocol 用户数据报协议
它是不可靠的无连接的协议。在数据发送之前,因为不需要建立连接,所以数据传输效率就会高
那么UDP和TCP就不太一样了,UDP它不会去确认目标是否能够通信,它只管往外发送数据,至于你能不能收到,有没有丢包一概
不管。
所以UDP一般用于传输少量数据
这两种协议各有各的好处,TCP更可靠稳定,而UDP虽然没有那么可靠但是效率高,所以目前应用比较多的就是UDP。
(5)网络层
网络层负责将数据传输到目标地址,这一层主要负责寻址和路由选择。主要由IP,ICMP两个协议组成
网络层将数据从主机A发送到主机B,那么我们发送过去的路径可能有很多种,比如数据从A出发,经过C,D再到B,也可以
从A出发,经过X,Y再到B,也就是说,通信双方之间的数据链路可能非常复杂。那么网络层就是负责找出一条相对更加高效的
路径将数据传递过去。传输的地址使用的是IP地址。IP地址跟我们住址有点类似,从大范围缩小到小范围。IP地址也有这样的
能力,通过不断的转发到更近的IP地址,最终到达目标地址,怎么选择就是网络层的事
(6)数据链路层
互联设备之间传送和识别的数据帧
有的买家在北京,有的买家在上海,所以要将仓库的货物分类封装。在数据链路层种,需要将比特流组装成字节的组合。
类似把地址相同的货物放到同一个集装箱中,只要这个集装箱到达目的地,货物自然也就到了,我们把这种比特流组成字节
的组合我们称之为数据帧
对数据进行处理封装成数据帧并传递和错误检测的层就是数据链路层
(7)物理层
它的主要作用就是通过光信号和电信号传输数据,物理层就是由实物所承载的,如将货物运送到目的地的交通工具属于
物理层
比如:网线,网卡(每一个网卡在出厂的时候就会设定一个世界上唯一的MAC地址,一个48bits的数据)
举一个完整的例子理解一下七层模型:
应用层 经理A 写好信件 阅读信件 经理B
表示层 翻译 将信件翻译成通用语言 将信件翻译
如果有需要的可能还有 提醒经理阅读信件
加密和压缩
会话层 秘书 找出收件人的地址 打开信封
传输层 司机 将信封带到邮局 将信封带回公司
--------------------------------------------------------------------------------------------------
网络层 分拣员 将信件按地址(地区)分开 处理包裹信息,让指定的人取件
数据链路层 包装工人 将同一个地区的信封打包 拆开来自各地的包裹
物理层 运输车 将信件运输过去 接收信件
一个包裹被经理写好,然后交给翻译将包裹的内容翻译成通用语言,接着翻译将被翻译的包裹给秘书,秘书在包裹上写好收件人的地址后
递给司机,司机将包裹送至邮局,邮局里面的分拣员将来自世界各地的包裹分类,把不同地区的包裹错开,接着包装工人将不同地区的包裹打包,
放进运输车送往世界对应的地址。
里面的经历,翻译,秘书,司机,分拣员,包装工人,运输车。
分别代表:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层在网络传输中扮演的角色。
写好信 翻译交给经理
翻译 秘书给翻译
写好地址 司机给秘书
传给网络 司机去拿货
地区分拣 查看地址后通知司机
打包信件 拆开包装
传输信件 收到信件
大致可以这么理解
3. 常用的与网络相关的设备以及各自的作用
我们知道如果两台电脑通过网线直连的话肯定是可以通信,此时这两台电脑就构成了一个小的局域网,局域网内只有两台设备。
但是这种应用场景非常少见,更常见的就是多台设备之间互联,也就是说一个局域网内有多个通信设备。但是问题是。一个计算机
只有一个网口,怎么让多台计算机互联?此时就需要用到交换机(switch)了。
交换机:可以提供大量的网络接口将多台网络设备连接成一个局域网。交换机内部拥有一根总线,交换机所有的接口都是挂载
这根总线上,意味着这些设备在物理层面都是互通的。
连接上同一个交换机上的网络设备如果要进行通信的话,可以使用一种广播的方式进行。
路由器:路由器的功能就是将不同的子网之间的数据进行传递,这个过程称之为寻址过程。
就比如快递公司发送邮件。邮件并不是瞬间到达目的地,而是通过不同分站的分拣,不断的接近最终地址。路由器寻址过程也是类似
的原理。通过最终地址,在路由表中进行匹配,通过算法去分析确定下一站的转发地址。这个地址可能是中间地址。
调制解调器(猫):是调制器和解调器的缩写,是一种硬件。我们网络信号的传递大部分都是光/电(模拟)信号,而计算机只能识别数字
信号(1/0),所以计算机从网络上接收到这些信号之后,需要将其翻译成计算机能够看懂的数字信号,有点类似A/D,D/A转换过程
4.网络数据的传输过程
看图示的封包和拆包的过程
注意:既然我们网卡都有一个唯一的MAC地址,或者说通过MAC地址就能找到另外一张网卡。那么有MAC地址不就可以实现了网络中
通信了,为什么还需要IP地址?
我们知道IP地址是网络层负责,网络接口层中负责MAC地址。而我们的以太网协议其实就是依靠MAC发送数据的
理论上,单单依靠MAC地址,北京的网卡就可以找到上海的网卡,但是有一个重大的缺点。以太网协议就会以广播的方式发送
数据包,也就是处于同一个局域网内的设备都能收到,这样做,效率较低,而且广播方式之局限于发送者所在的子网,也就是说
只要通信双方不在同一个子网,广播不能传递出去的。这种设计是不合理的,否则互联网上每一台计算机都会收到数据包,那么就会
造成网络拥堵设置网络瘫痪。
因为,必须找到一种方法,去区分哪些MAC地址是属于同一个子网络,哪些不是。如果是同一个子网络,就会采用广播的方式
如果不是,就采用"路由选择"
这才导致了"网络层"的诞生,它的作用就是引进一套新的地址,使得我们去区分不同得计算机是否属于同一个子网。这套地址
称之为"网络地址",简称"网址"
于是在出现"网络层"之后,每台计算机有了两种地址,一种MAC地址,另外一种是网络地址。MAC地址是由硬件(网卡)决定得,
一般不能改动,网络地址则是由软件决定得,是可以改动的。
所以网络地址就是帮助我们找到计算机所在的子网络,MAC地址帮助我们找到自网络的目标网卡。
如果只有IP地址而没有MAC地址的话,就相当于根据IP找到子网,但是子网可能有很多主机,不能确定要找的是谁
5. 互联网地址(IP地址)
IP地址:Internet Protocol Address的缩写 "网际协议地址"
常见的IP地址分成两类:
IPv4 --> 32bits IPv6 -->128bits
目前应用最多还是IPv4,IPv6开始在教育网中使用。
理论上IPv4能够提供2^32个IP,而IPv6能够提供2^128个IP。
IPv4地址有32bits,那么这32bits有什么含义?
一般把一个ipv4地址分成两部分:
像日常生活中的电话号码一样:
区号 + 主机号 --> 0731 82551678
IPv4 32bits:
网段号:用来标识哪一个网段
比如处于同一个路由器下的主机的IP的网段,就是一样
在IP地址的连续的高位上
主机号:用来标识子网中不同的主机
处于同一个子网内的主机的IP地址的主机号一定不一样
在IP地址的连续的低位上
如: 192.168.31.8(IP地址用点分十进制标识)
每8bits一个数字,中间用.隔开
网段号和主机号各占多少位?根据不同地址类型来决定
但是总的来说:网段号+主机号就是32bits
IP地址分类:
类别 分段 IP地址范围 私有地址范围
A 0网段号(7bits)+主机号(24bits) 0.0.0.0 - 127.255.255.255 10.0.0.0 - 10.255.255.255
B 10网段号(14bits)+主机号(16bits) 128.0.0.0 - 191.255.255.255 172.16.0.0 - 172.31.255.255
C 110网段号(21bits)+主机号(8bits) 192.0.0.0 - 223.255.255.255 192.168.0.0 - 192.168.255.255
D 1110多播组号(28bits) 224.0.0.0 - 239.255.255.255
E 11110(留着以后用) 240.0.0.0 - 247.255.255.255
在设置IP地址的时候,一般需要设置另外一个称之为子网掩码的东西。
NETMASK子网掩码:
就是用来指定一个IP地址中,哪些bits是网段号,哪些bits是主机号
简单的说,netmask为1的就是网段号,为0的就是主机号
netmask: 255.255.255.0
-->那么就意味着IP中的高三字节为网段号,低一字节为主机号。
那么我们可以通过这个判断两个IP是否属于同一个网段
如:
192.168.1.4和192.168.3.5就不是同一个网段
并不是所有的掩码都是合法的。
如:
255.255.114.0
---> 1111 1111 1111 1111 0111 0010 0000 0000 //不合法
255.255.192.0
---> 1111 1111 1111 1111 1100 0000 0000 0000 //合法
也就是说1和0必须是连续的
我们知道一台计算机可以拥有一个独立的IP地址,一个局域网也可以有一个独立的地址(对外就好像只有一台计算机),对于目前
广泛使用IPv4地址,它的资源肯定是有限的,一台计算机一个IP是不现实的,往往是一个局域网才拥有一个IP。
在网络上通信时,必须知道对方的IP地址。实际上数据包中已经附带了IP地址,把数据包发送到路由器后,路由器会根据IP地址找到
对方的位置,完成一次数据的传递。
那么一个局域网才能拥有一个独立IP,换句话说,IP地址只能定位到一个局域网,无法定法到一台计算机 -->再依靠MAC地址,而且
MAC地址已经被写死在网卡(某些牛逼的人通过一些技术也是可以修改)。局域网中的路由器会记录每台计算机的MAC地址
有了IP地址和MAC地址,虽然能够找目标计算机,但是仍然不能通信。计算机能够收取数据包,但是它并不知道要给哪个网络程序
来处理,所以通信失败。
为了区分不同的网络程序,计算机会分每一个网路程序分配一个独一无二的端口号(Port Number)。
6.端口号
TCP和UDP采用16bits的端口号用来标识网络程序
所以网络程序从传输层额角度来看:
TCP应用
UDP应用
TCP端口号和UDP的端口号是独立的。
也就是说TCP端口号20和UDP端口号20是两个不同的端口。
所以一台主机上的网络应用程序的地址:
MAC地址 + IP地址 + 传输层协议(TCP/UDP) + 端口号
端口号并不是你想指定什么就什么,而是由IANA机构管理
比如:ftp服务器 -->21端口
http服务器 -->80端口
.....
公共端口 0-1023 是已经被分配的
7.网络字节序
网络上传输数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节
的时候,它是把这个字节当成高字节还是低字节处理,必须探究
UDP/TCP/IP协议规定:把接收到的第一个字节当作高字节看待,要求发送发送的第一个字节必须是高位字节;而发送端发送数据的时候
第一个字节应该是该数值在内存中的起始地址对应的那个字节,也就是说第一个字节(高字节)存放在低地址--->大端存放
所以说,网络字节序是大端字节序
二. socket套接字
Socket是一个编程接口,作用就是用来实现网络上不同主机的应用程序进行双向通信。
套接字当成一种特殊的文件描述符,也就意味着我们使用套接字实现网络通信可以使用read/write。比如:客户端可以用write
发送网络数据,服务端可以使用read接收数据
要通过互联网进行通信,至少需要一对套接字,其中一个运行在客户端,称之为Client socket,另外一个运行在服务端,称之为
server socket。
socket可以分成三种类型:
(1)流式套接字(SOCK_STREAM)
流式套接字用于提供面向连接,可靠的数据传输服务
主要针对于传输层协议为TCP协议的应用
如果我们写代码利用TCP来传输数据,需要一个流式套接字
(2)数据报套接字(SOCK_DGRAM)
数据报套接字提供一种无连接的服务(不能保证数据传输的可靠性)
主要针对于传输层协议为UDP协议的应用
(3)原始套接字(SOCK_RAW)
原始套接字可以直接跳过传输层读取没有处理的IP数据包。因此,如果要访问其他协议发送的数据必须使用原始套接字
三:什么是TCP的三次握手和四次挥手?
TCP(传输控制协议)是一种可靠的传输协议,用于在计算机网络上建立可靠的连接。在建立和终止TCP连接时,使用了三次握手和四次挥手的机制。
三次握手(Three-Way Handshake):
1. 第一次握手:客户端发送一个序列号为seq(假设是1000)的带有 SYN(建立连接)标志的TCP包,指示客户端请求建立连接。
2. 第二次握手:服务器接收到客户端的请求后,发送序列号(假设为2000)的一个带有 SYN/ACK(建立连接/确认收到)标志的TCP包,表示同意建立连接。
3. 第三次握手:客户端收到服务器的确认后,再次发送一个带有 ACK(确认收到)标志的TCP包,表示连接已经建立。
其中确认包Ack是对方的TCP包的序号报+1
其中一共涉及3个包的:seq,SYN,ACK
也就是说ACK = seq(对方发来的TCP包序列号) + 1;
通过这个三次握手过程,客户端和服务器都确认了彼此的能力和意愿,以建立可靠的连接。
四次挥手(Four-Way Handshake):
1. 第一次挥手:当客户端想要终止连接时,发送一个带有 FIN(结束)标志的TCP包,表示不再发送数据。
2. 第二次挥手:服务器收到客户端的结束通知后,发送一个带有 ACK(确认)标志的TCP包,表示已经收到请求,并准备关闭连接。
3. 第三次挥手:服务器结束发送数据后,发送一个带有 FIN(结束)标志的TCP包,表示服务器不再发送数据。
4. 第四次挥手:客户端接收到服务器的结束通知后,发送一个带有 ACK(确认)标志的TCP包,表示已经接收到服务器的请求,并且同意关闭连接。
通过这个四次挥手过程,客户端和服务器都确认了双方不再发送数据,关闭了连接。
三次握手和四次挥手过程都是为了确保连接的可靠性和一致性,以及在建立和终止连接时的双向确认。
四.基础知识总结:
网络编程是指通过计算机网络实现数据传输和通信的编程方式。
在网络编程中,开发人员使用编程技术来创建网络连接、发送和接收数据,并进行网络通信。
网络编程可以涉及多个层次和协议,包括以下方面:
Socket 编程:Socket 是网络编程中的一种编程接口,它提供了一种机制,使得不同计算机上的应用程序能够通过网络进行通信。
Socket 编程通过创建套接字(socket)对象,使用不同的网络协议(如 TCP 或 UDP)来建立连接、发送和接收数据。网络协议:网络编程需要了解和使用各种网络协议,如 TCP/IP、HTTP、FTP、SMTP 等。
开发人员需要理解这些协议的工作原理和规范,以便正确地构建网络应用程序。客户端-服务器模型:网络编程通常使用客户端-服务器模型,其中客户端应用程序通过网络连接到服务器应用程序,发送请求并接收响应。
客户端发送请求,服务器接收并处理请求,然后发送响应给客户端。数据传输和序列化:在网络编程中,数据需要在客户端和服务器之间传输。
数据可能以不同的格式进行序列化,如 JSON、XML 等。
开发人员需要处理数据的序列化和反序列化,以便在网络上进行正确的数据传输。网络安全和身份验证:网络编程也涉及网络安全和身份验证,以确保数据的安全性和保密性。
开发人员需要使用加密、数字证书、身份验证等技术来保护网络通信的安全性。
网络编程使得计算机之间可以高效地通信和协作,扩展了应用程序的能力和范围。
五.TCP套接字编程
1. TCP套接字编程的流程
(1)TCP网络应用大概的流程过程
建立连接(相当于微信加好友)
三次握手
发送/接收网络数据(聊天):
write/send/sendto
read/recv/recvfrom
关闭连接:(聊完删除好友)
四次挥手
建立连接成功
为什么需要三次握手这种机制?
通信双方能够成功通信的前提条件是是双方建立好连接。
那么双方能够连接到一起的条件是什么?
必须双方都能收和发。
所以三次握手实际上就是一个测试能不能成功建立连接的过程
第一次握手实际上测试了客户端能不能发,第二次握手实际上测试服务端能不能收和发,第三次握手测试客户端 能不能收。
三次握手的具体过程:
先了解一下TCP数据包结构:
(1)序号:Seq占32位,用来标识从计算机A发送到计算机B的数据包的编号
(2)确认号:Ack确认号占32位,客户端和服务器都可以发送,Ack = Seq + 1。
(3)标志位:每个标志位占1bit,共有6个,分别为URG,ACK,PSH,RST,SYN,FIN
ACK:确认序号有效
RST:重置连接
SYN:建立一个新的连接
FIN:断开一个连接
使用connect建立连接时,客户端和服务器端互相发送这三个数据包,如图所示:
总结图示的过程:
(1)当客户端调用connect函数后,TCP协议就会组装一个数据报,并设置SYN标志位,表示这个数据包就是用来
建立连接的,同时会生成一个序号填充进"序号Seq(1000)",将这个数据包发送给服务器。客户端就进入了SYN-SENT状态。
(2)服务器收到这个数据包之后,检测SYN标志位,就知道这个是某个客户端发来用来建立连接的"请求包"。
服务器也会组装一个数据包,并设置了SYN和ACK,生成一个序号Seq(2000)。服务器会将前面接收的数据包的序号+1
填进确认号Ack(Seq+1:1001)里面,将这个包发送给客户端,进入SYN-RCVD状态。
(3)客户端收到数据包之后,检测到已经设置了SYN和ACK标志位,就知道这个是服务器发来的"确认包",客户端会
检测这个包的确认号Ack的值是否是1001,如果是说明回应成功。
接下来,组装数据包,设置ACK标志位,表示客户端正确接收了服务器发来的"数据包",同时将前面的包的Seq+1(2001)
填充到这个包的确认号Ack,客户端将这个数据包发送出去之后,进入ESTABLISHED状态
(4)服务器收到数据包之后,检测ACK标志位,检查这个包的确认号是否是2001,如果是表示连接建立成功
此时进入ESTABLISHED状态
当客户端和服务器都进入ESTABLISHED状态,连接建立成功,可以互相收发数据了。
四次挥手:
建立连接是非常重要的,他是数据正确传输的前提,断开连接也是,它让计算机释放不再使用的资源
总结:
(1)客户端调用close函数之后,向服务器发送一个FIN数据包,进入FIN_WAIT_1状态
(2)服务器收到FIN数据包之后,检测到了FIN标志位,于是向客户端发送一个ACK数据包,进入CLOSE_WAIT状态
注意:服务器收到请求后并不是立即断开连接,而是先向客户端回应
(3)客户端收到ACK包进入了FIN_WAIT_2状态,等待服务器发送最后的FIN包
(4)服务器准备完毕之后,可以断开连接,于是主动向客户端发送FIN包,进入LAST_ACK状态
(5)客户端收到服务器的FIN包之后,再次向服务器发送ACK包,进入TIME_WAIT状态。
(6)服务器收到客户端的ACK包之后,断开连接,关闭套接字,进入CLOSED状态
2.TCP编程的流程
图示
TCP server:
socket():创建一个套接字
bind():把一个套接字与网络地址相绑定
如果你想让其他人主动来找你,你就需要绑定一个地址
不调用bind,并不代表你的socket就没有地址,如果不绑定,内核会自动帮你指定一个地址
listen():"监听模式"
accept():接收客户端连接请求
多次调用accept可以与不同的客户端建立连接
read/recv/recvfrom or write/send/sendto
close/shutdown:四次挥手
TCP Client:
socket()
bind():可要可不要
connect:发起连接请求
read/recv/recvfrom or write/send/sendto
close/shutdown
3.socket具体的API函数解析
(1)socket:创建一个套接字
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
@domain:指定域或者协议族
socket接口不仅仅局限于TCP/IP,它还有bluetooth,本地通信....
每一种通信下面都有许多的协议,大概协议如下:
AF_INET IPv4协议族
AF_INET6 IPv6协议族
AF_UNIX, AF_LOCAL Unix域协议族 -->IPC手段
@type:指定套接字的类型
SOCK_STREAM:流式套接字 -->TCP
SOCK_DGRAM: 数据报套接字 -->UDP
SOCK_RAW:原始套接字
@protocol:指定应用层协议,可以指定为0(不知名的私有应用协议)
返回值:
成功的话返回一个套接字文件描述符(>2)
失败返回-1,同时errno被设置
(2)网络地址结构体
socket接口它不仅仅用于IPv4,也可以用于IPv6.....,那么不同的协议的地址是不一样的。
但是我们在编程的时候,必须指定网络地址,所以需要对地址进行一个统一(标准化)
用这样的两个结构体来描述网络通信的地址:
通用网络地址结构体:
struct sockaddr
{
sa_family_t sa_family;//指定协议族
char sa_data[14];//包含套接字中目的地址和端口信息
};
这个结构体的缺陷是把IP地址和端口地址混在一起保存在sa_data中。
IPv4专用地址结构体:
struct sockaddr_in
{
sa_family_t sin_family; //指定协议族 AF_INET
in_port_t sin_port; //指定端口号 1024-65535
struct in_addr sin_addr;//指定IP地址 怎么赋值呢?IP地址转换。
unsigned char sin_zero[8];//填充8个字节 无实际含义 只是为了大小和其他协议族地址结构体一样
};
struct in_addr
{
in_addrt_t s_addr; //in_addrt_t uint32_t unsigned int
}
这个结构体把IP地址和端口号分开保存了
sockaddr常用于bind,connect,recvfrom,sendto等函数的参数。
(3)IP地址转换函数
我们知道IP地址是以点分十进制的形式存在的,实际上转换成一个无符号的32bit的数据
SYNOPSIS
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
将点分十进制的字符串转换为IP
3.1 int inet_aton(const char *cp, struct in_addr *inp);
a:addr 点分十进制的字符串
n:network 网络地址
@cp:指向要转换的点分十进制的字符串 -->"192.168.31.8"
@inp:指向一个IP结构体 struct in_addr
用来保存转换后的IP的地址
返回值:
成功返回0,失败返回-1,同时errno被设置
3.2
in_addr_t inet_addr(const char *cp);
将cp指向的"点分十进制的字符串"转换为IP网络地址
3.3
in_addr_t inet_network(const char *cp);
与inet_addr函数类似
3.4
char *inet_ntoa(struct in_addr in);
把网络地址IP转换成点分十进制
例子:
struct sockaddr_in Serv;
Serv.sin_family = AF_INET;
Serv.sin_port = 1111;//不可行
Serv.sin_addr.s_addr = inet_addr("192.168.31.8");
//inet_aton("192.168.31.8", &Serv.sin_addr);
(4)整数在主机字节序与网络字节序之间的转换函数
NAME
htonl, htons, ntohl, ntohs - convert values between host and network byte order
SYNOPSIS
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h:host n:network
l:long 32bits
s:short 16bits
htonl:把一个32bits的整数的主机字节序转换成网络字节序
htons:把一个16bits的整数的主机字节序转换成网络字节序
ntohl:把一个32bits的整数的网络字节序转换成主机字节序
ntohs:把一个16bits的整数的网络字节序转换成主机字节序
Serv.sin_port = htons(1111);//可行
(5)bind
NAME
bind - bind a name to a socket
把一个IPv4的网络地址绑定到一个socket上面去
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
@sockfd:要绑定地址的套接字描述符
@addr:通用的网络地址结构体的指针
@addrlen:第二个参数指向的地址结构体的长度
返回值:
成功返回0,失败返回-1
(6)listen:让套接字进入"监听模式"
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
@sockfd:要进入监听模式的套接字
@backlog:同时能够处理连接请求的数目
如: 5,10.....
返回值:
成功返回0,失败返回-1,同时errno被设置
(7)accept
用于TCP server接收来自一个客户端的连接请求
NAME
accept - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
@sockfd:套接字
@addr:网络地址结构体的指针,用来保存客户端的地址信息的
@addrlen:用来保存客户端地址结构体的长度
返回值:
成功的返回与该客户端的连接套接字描述符,后续与该客户端通信用的都是这个套接字描述符
失败返回-1,同时errno被设置
(8)connect
主要用于客户端主动向服务器发起连接请求
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
@sockfd:套接字
@addr:对方的地址,服务器的地址
@addrlen:对方的地址结构体的长度
返回值:
成功返回0,失败返回-1
(9)往套接字上面发送数据
write/send/sendto这三个函数。TCP都可以用,UDP只能sendto
NAME
send, sendto - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
9.1 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
@sockfd:套接字
@buf:保存你即将要发送的数据
@len:你想发送的数据的长度
@flags:一般为0
返回值:
成功返回实际发送的字节数,失败返回-1,同时errno被设置
9.2 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
@dest_addr:指定接收方的地址。
如果是TCP通信,此处可以省略,因为TCP是面向连接的,意味着在数据发送之前就已经建立连接了。
如果是UDP通信,需要指定
@addrlen:对方地址的长度
(10)从套接字上接收数据
read/recv/recvfrom
NAME
recv, recvfrom - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
10.1 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv的参数与read类似,省略
10.2 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
@src_addr:用来保存发送方的地址
TCP可以不用管,因为TCP肯定知道是谁发的,填NULL
UDP,也可以填NULL,表示不关系是谁发,也不会影响别人给你发数据
@addrlen:用来保存发送者地址的实际的长度
返回值:返回实际收到的数据的字节数,失败返回-1,同时errno被设置
(11) 关闭套接字 close/shutdown
NAME
shutdown - shut down part of a full-duplex connection
SYNOPSIS
#include <sys/socket.h>
int shutdown(int sockfd, int how);
@sockfd:套接字
@how:关闭的方式,有三种
SHUT_RD:关闭读
SHUT_WR:关闭写
SHUT_RDWR:关闭读写 -->close(sockfd);
返回值:
成功返回0,失败返回-1,同时errno被设置
六.基于linux gcc编译器使用TCP/UDP协议编写服务器和客户端(使用C语言):
源代码:TCP服务器
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char *argv[])
{
if(argc != 3)
{
printf("Usage:%s <IP> <PORT>\n",argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed!!");
return -1;
}
//2.绑定服务器的IP和PORT
struct sockaddr_in Serv;
Serv.sin_family = AF_INET;
Serv.sin_port=htons(atoi(argv[2]));
Serv.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(sockfd,(struct sockaddr *)&Serv,sizeof(Serv));
if(ret == -1)
{
perror("bind failed!!");
close(sockfd);
return -1;
}
//3.监听模式
ret = listen(sockfd,10);
if(ret == -1)
{
perror("listen failed!!");
close(sockfd);
return -1;
}
while(1)
{
struct sockaddr_in clie;
socklen_t len = sizeof(clie);
//4.阻塞等待连接
int confd = accept(sockfd,(struct sockaddr *)&clie,&len);
if(confd == -1)
{
perror("confd failed!!");
continue;
}
printf("connect [%s][port:%d] sucess!!!!!\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port));
//创建一个新的进程与客户端通信
pid_t pid = fork();
if(pid >0)
{
close(confd);
}
else if(pid == 0)//子进程
{
while(1)
{
char recv_buf[1024]={0};
int ret = recv(confd,recv_buf,1023,0);
if(ret > 0)
{
printf("recv data[%s]:%s\n",inet_ntoa(clie.sin_addr),recv_buf);
if(strncmp(recv_buf,"byebye",6) == 0)//接收到了退出指令
{
printf("connect [%s][port:%d] sucess exit!!\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port));
close(confd);
exit(0);
}
char *buf = "OK!!\n";
send(confd,buf,strlen(buf),0);
}
}
}
else
{
close(confd);
perror("fork failed");
continue;
}
}
}
TCP客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char *argv[])
{
if(argc != 3)
{
printf("Usage:%s <IP> <PORT>\n",argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed!!");
return -1;
}
//2.指定服务器的地址
struct sockaddr_in Serv;
Serv.sin_family = AF_INET;
Serv.sin_port=htons(atoi(argv[2]));
Serv.sin_addr.s_addr = inet_addr(argv[1]);
//3.连接服务器
int ret = connect(sockfd,(struct sockaddr *)&Serv,sizeof(Serv));
if(ret == -1)
{
perror("connect failed!!");
return -1;
}
while(1)
{
char send_buf[1024]={0};
fgets(send_buf,1023,stdin);
send(sockfd,send_buf,strlen(send_buf) - 1,0);
if(strncmp(send_buf,"byebye",6) == 0)
{
break;
}
char buf[1024]={0};
recv(sockfd,buf,1023,0);
printf("recv data[%s]:%s\n",argv[1],buf);
}
//4.关闭套接字
shutdown(sockfd,SHUT_RDWR);
return 0;
}
源代码:UDP服务器
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char *argv[])
{
if(argc != 3)
{
printf("Usage:%s <IP> <PORT>\n",argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket failed!!");
return -1;
}
//2.指定服务器的地址
struct sockaddr_in Serv;
Serv.sin_family = AF_INET;
Serv.sin_port=htons(atoi(argv[2]));
Serv.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(sockfd,(struct sockaddr *)&Serv,sizeof(Serv));
if(ret == -1)
{
perror("bind failed!!");
close(sockfd);
return -1;
}
//3.数据传输
while(1)
{
struct sockaddr_in clie;
socklen_t len = sizeof(clie);
char recv_buf[1024]={0};
int ret = recvfrom(sockfd,recv_buf,1023,0,(struct sockaddr *)&clie,&len);
if(ret > 0)
{
printf("recv data[%s][port:%d]:%s\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port),recv_buf);
if(strncmp(recv_buf,"byebye",6) == 0)//接收到了退出指令
{
printf("[%s][port:%d] exit!!!\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port));
continue;
}
char *buf = "OK!!\n";
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&clie,len);
}
}
//4.关闭套接字
close(sockfd);
}
UDP客户端:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc,char *argv[])
{
if(argc != 3)
{
printf("Usage:%s <IP> <PORT>\n",argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1)
{
perror("socket failed!!");
return -1;
}
//2.指定服务器的地址
struct sockaddr_in Serv;
Serv.sin_family = AF_INET;
Serv.sin_port=htons(atoi(argv[2]));
Serv.sin_addr.s_addr = inet_addr(argv[1]);
//3.数据传输
while(1)
{
struct sockaddr_in clie; //保存服务器的ip地址和端口的
socklen_t len = sizeof(clie);
char buf[1024];
fgets(buf,1023,stdin);
sendto(sockfd,buf,strlen(buf) - 1,0,(struct sockaddr *)&Serv,len);
if(strncmp(buf,"bye",3) == 0)
{
break;
}
//收
char recv_buf[1024]={0};
int ret = recvfrom(sockfd,recv_buf,1023,0,(struct sockaddr *)&clie,&len);
if(ret > 0)
{
printf("recv data[%s][port:%d]:%s\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port),recv_buf);
if(strncmp(recv_buf,"byebye",6) == 0)//接收到了退出指令
{
printf("[%s][port:%d] exit!!!\n",inet_ntoa(clie.sin_addr),ntohs(clie.sin_port));
continue;
}
}
}
//4.关闭套接字
close(sockfd);
}
编程简要:
TCP服务器:
创建套接字,绑定服务器的ip和端口,进入监听模式,等待客户端连接获取客户端网络套接字,创建子进程和客户端通信,设定通信结束条件调用exit(0)让子进程退出。
TCP客户端:
创建套接字,指定需要通信的服务器的ip和端口,向该服务器发送连接请求,请求成功后开始和服务器收发数据。
UDP服务器:
创建套接字,绑定ip和端口,创建空的网络地址结构体,等待客户端对自己发消息,然后将客户端的地址保存到结构体里,开始于客户端对话通信。
UDP客户端:
创建套接字,指定服务器的ip和端口,直接发送数据给服务器。
(以上编程简要是小编随意梳理的思路(并不完善),读者可根据源代码自行梳理便于自己理解的思路)
注意事项:以上源代码仅针对同一局域网之间的设备通信。TCP是基于双方确认连接后再通信的传输协议(稳定),UDP是不基于连接的,直接通信的传输协议(较不稳定,但效率高,所以普及比TCP协议广泛)。
版权归原作者 你的麦克枫 所有, 如有侵权,请联系我们删除。