技术学习 Linux Linux网络编程实践 lycheeKing 2022-12-20 2024-10-27 Linux网络编程实践 [TOC]
一、Linux网络编程框架 1、网络是分层的 (1)OSI 7层模型
(2)网络为什么要分层
1)各层次之间是独立的 。某一层并不需要知道它的下一层是如何实现的,而仅仅需要知道该层通过层间的接口所提供的服务。这样,整个问题的复杂程度就下降了。也就是说上一层的工作如何进行并不影响下一层的工作,这样我们在进行每一层的工作设计时只要保证接口不变可以随意调整层内的工作方式。
2)灵活性好 。当任何一层发生变化时,只要层间接口关系保持不变,则在这层以上或以下层均不受影响。当某一层出现技术革新或者某一层在工作中出现问题时不会连累到其它层的工作,排除问题时也只需要考虑这一层单独的问题即可。
3)结构上可分割开 。各层都可以采用最合适的技术来实现。技术的发展往往不对称的,层次化的划分有效避免了木桶效应,不会因为某一方面技术的不完善而影响整体的工作效率。
4)易于实现和维护 。这种结构使得实现和调试一个庞大又复杂的系统变得易于处理,因为整个的系统已经被分解为若干个相对独立的子系统。进行调试和维护时,可以对每一层进行单独的调试,避免了出现找不到、解决错问题的情况。
能促进标准化工作 。因为每一层的功能及其所提供的服务都已有了精确的说明。标准化的好处就是可以随意替换其中的某一层,对于使用和科研来说十分方便。
(3)网络分层的具体表现
研究时应在同一层次,若研究两个东西,a在应用层,则研究b也在应用层研究,同理研究网络层,则二者都在网络层
2、TCP/IP协议引入 详解:https://blog.csdn.net/qq_38560742/article/details/88398270
(1)TCP/IP协议是用的最多的网络协议实现
(2)TCP/IP分为4层,对应OSI的7层
应用层 对应 应用/表示/会话层 (e-mail、FTP等)
传输层 对应 传输层 (TCP、UDP)
网络层 对应 网络层 (IP、ICMP、IGMP)
链路层 对应 数据链路/物理层 (设备驱动程序和接口卡)
(3)我们编程时==最关注应用层,了解传输层==,网际互联层和网络接入层不用管
3、BS和CS 详解:https://blog.csdn.net/qq_38056704/article/details/80730532`
https://www.cnblogs.com/forever5325/p/9529092.html
(1)CS架构介绍(client server,客户端服务器架构 ),一种服务器就要一个客户端,使用多个服务器就要安装多个服务端。
C/S架构是一种典型的两层架构,其全程是Client/Server,即客户端服务器端架构,其客户端包含一个或多个在用户的电脑上运行的程序,而服务器端有两种,一种是数据库服务器端,客户端通过数据库连接访问服务器端的数据;另一种是Socket服务器端,服务器端的程序通过Socket与客户端的程序通信。
C/S架构也可以看做是胖客户端架构 。因为客户端需要实现绝大多数的业务逻辑和界面展示。这种架构中,作为客户端的部分需要承受很大的压力,因为显示逻辑和事务处理都包含在其中,通过与数据库的交互(通常是SQL或存储过程的实现)来达到持久化数据,以此满足实际项目的需要。
1 2 3 4 5 6 7 8 9 C/S 架构的优缺点 优点: 1. C/S架构的界面和操作可以很丰富。 2. 安全性能可以很容易保证,实现多层认证也不难。 3. 由于只有一层交互,因此响应速度较快。 缺点: 1. 适用面窄,通常用于局域网中。 2. 用户群固定。由于程序需要安装才可使用,因此 不适合面向一些不可知的用户。 3. 维护成本高,发生一次升级,则所有客户端的程序 都需要改变。
(2)BS架构介绍(broswer server,浏览器服务器架构),避免了cs架构的弊端。
B/S架构的全称为Browser/Server,即浏览器/服务器结构。Browser指的是Web浏览器,极少数事务逻辑在前端实现,但主要事务逻辑在服务器端实现,Browser客户端,WebApp服务器端和DB端构成所谓的三层架构。B/S架构的系统无须特别安装,只有Web浏览器即可 。B/S架构中,显示逻辑交给了Web浏览器,事务处理逻辑在放在了WebApp上,这样就避免了庞大的胖客户端,减少了客户端的压力。因为客户端包含的逻辑很少,因此也被成为瘦客户端 。
1 2 3 4 5 6 7 8 9 10 11 12 B/S架构的优缺点 优点: 1. 客户端无需安装,有Web浏览器即可。 2. BS架构可以直接放在广域网上,通过一定的权限控制实现多客户访问的目的,交互性较强3. BS架构无需升级多个客户端,升级服务器即可缺点: 1. 在跨浏览器上,BS架构不尽如人意。 2. 表现要达到CS程序的程度需要花费不少精力。 3. 在速度和安全性上需要花费巨大的设计成本,这是BS架构的最大问题。4. 客户端服务器端的交互是请求-响应模式,通常需要刷新页面,这并不是客户乐意看到的。(在Ajax风行后此问题得到了一定程度的缓解)
summary:服务器和客户端都是一个程序。
二、TCP协议的学习 APP(调用socket接口API) -> TCP -> IP
IP协议是无连接的通信协议,IP不会占用两个设备之间通信的线路,IP实际上主要负责将每个数据包路由至目的地,但是 IP协议并没有能够确保数据包是否到达,传过去的数据包是否按照顺序排列,所以IP数据包是不可靠的 。而解决数据不可靠的问题就是由TCP协议来完成
1、关于TCP理解的重点 (1)TCP协议工作在传输层,对上服务socket接口,对下调用IP层 。
(2)TCP协议面向连接,通信前必须先3次握手建立连接关系后才能开始通信 。
(3)TCP协议提供可靠传输,不怕丢包、乱序等 。
2、TCP如何保证可靠传输 (1)TCP在传输有效信息前要求通信双方必须先握手,建立连接才能通信
(2)TCP的接收方收到数据包后会ack给发送方,若发送方未收到ack会丢包重传(直到发送方收到ack)
(3)TCP的有效数据内容会附带校验,以防止内容在传递过程中损坏
(4)TCP会根据网络带宽来自动调节适配速率(滑动窗口技术)(比如第一次发送64字节,全都正常发送并得到ack信号,则下次可以加大发送的字节数)
(5)发送方会 给 各接收方分割报文 编号,接收方会校验编号,一旦顺序错误即会重传。
ECC校验详解:https://blog.csdn.net/wzsalan/article/details/79842220
3、TCP的三次握手 详解:https://baijiahao.baidu.com/s?id=1618114723935605183&wfr=spider&for=pc
https://www.cnblogs.com/Cubemen/p/10803275.html
(1)建立连接需要三次握手
ACK:确认序号标志,为1表示确认号有效,为0表示报文中不含有确认信息,确认号无效
SYN:同步序号,用于建立连接过程
第一次握手:客户端发送syn包(seq=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
(2)建立连接的条件:服务器listen时客户端主动发起connect
服务器维护时则不处于listen状态,客户端无法访问;建立连接后,客户端和服务器可进行任意的双向的自由通信,二者没有先后顺序的限制。这是因为TCP没有做任何限制,但上层应用层会做出一些限制。比如规定二者谁先发一次,谁后发一次。
通信前建立连接(三次握手),通信后关闭连接(四次握手)。
4、TCP的四次挥手 (1)关闭连接需要四次握手
(2)服务器或者客户端都可以主动发起关闭
注:这些握手协议已经封装在TCP协议(就是一段代码实现的)内部,socket编程接口平时不用管.
5、基于TCP通信的服务模式 (1)具有公网IP地址的服务器(或者使用动态IP地址映射技术,减少公网网站对IP地址的需求),客户端发送给服务器端的数据包含了自己的IP地址,所以服务器可直接回复客户端。
(2)服务器端socket、bind、listen后处于监听状态
(3)客户端socket后,直接connect去发起连接。
(4)服务器收到并同意客户端接入后会建立TCP连接,然后双方开始收发数据,收发时是双向的,而且双方均可发起
(5)双方均可发起关闭连接
6、常见的使用了TCP协议的网络应用 (1)http(在这可看成一个程序,本身也是一个协议)、ftp:底层都是使用TCP协议的
(2)QQ服务器
(3)mail服务器
三、socket编程接口介绍 详解:https://www.cnblogs.com/sammyliu/p/5225623.html https://blog.csdn.net/zqixiao_09/article/details/79166462 https://www.cnblogs.com/jiangzhaowei/p/8261174.html
1、建立连接 (1)socket。socket函数类似于open,用来打开一个网络连接,如果成功则返回一个网络文件描述符(int类型),之后我们操作这个网络连接都通过这个网络文件描述符。
1 2 3 4 5 6 #include <sys/types.h> #include <sys/socket.h> int socket (int domain, int type, int protocol) ;domian指协议族/域,IPV4采用AF_INET; type是套接口类型,对于TCP采用SOCK_STREAM; protocol一般取为0 (表示使用默认协议).成功返回一个小的非负整数值,与文件描述符类似
(2)bind
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/types.h> #include <sys/socket.h> int bind (int sockfd, const struct sockaddr *addr,socklen_t addrlen) ;当socket函数返回一个文件描述符时,只是存在于其协议族的空间中,并没有分配一个具体 的协议地址(IPV4和端口号的组合),bind函数可以将一组固定的地址绑定到sockfd上。成 功返回0 ,失败返回-1 。 sockfd是socket()函数返回的描述符;类似于open以及fcntl函数 addr指定了想要绑定的IP和端口号,均要使用网络字节序-即大端模式;兼容两种IP 地址,IPV4、IPV6 addrlen是前面struct sockaddr (与sockaddr_in 等价)的长度。
(3)listen
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <sys/types.h> #include <sys/socket.h> int listen (int sockfd, int backlog) ;如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听 这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收 到这个请求。 listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket 可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的, listen函数将socket变为被动类型的,等待客户的连接请求。 RETURN VALUE On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
(4)connect
1 2 3 4 5 6 7 8 9 10 11 #include <sys/types.h> #include <sys/socket.h> int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的 socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数 来建立与TCP服务器的连接。 通过connect函数建立于TCP服务器的连接,实际是发起三次握手过程,仅在连 接成功或失败后返回。参数sockfd是本地描述符,addr为服务器地址,addrlen 是socket地址长度。
(5)TCP服务器端依次调用==socket()、bind()、listen()==之后,就会监听指定的 socket 地址了。TCP客户端依次调用==socket()、connect()==之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用 ==accept()== 函数去接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ; 参数sockfd: 参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口, 当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号 正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址 和一个端口号。 参数addr: 这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过 某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣, 那么可以把这个值设置为NULL 。 参数addrlen: 如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所 占有的字节个数。同样的,它也可以被设置为NULL 。 如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接 字来完成与客户的通信。 注意: accept 默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。 此时我们需要区分两种套接字, 监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字) 连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数 返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。 一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一 直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器 完成了对某个客户的服务,相应的已连接socket描述字就被关闭。 自然要问的是:为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么 它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。 连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号`
2、发送和接收 详解:https://www.cnblogs.com/blankqdb/archive/2012/08/30/2663859.html
(1)send和write
(2)recv和read
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示读的是文件的末尾(结束部分),小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
1 2 3 4 5 6 7 8 9 10 11 12 #include <unistd.h> ssize_t read (int fd, void *buf, size_t count) ;ssize_t write (int fd, const void *buf, size_t count) ;#include <sys/types.h> #include <sys/socket.h> 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) ;对于函数所使用的参数详解查阅man手册
3、辅助性函数:主要用于进行IP地址的转换,点分十进制与二进制方式 (1)inet_aton、inet_addr、inet_ntoa(过去经常使用的接口,很多现存代码可以看到)
(2)inet_ntop、inet_pton(现在推荐使用的接口)
若程序只支持IPV4,则(1)、(2)无区别。若用IPV6则选择(2)
4、表示IP地址相关数据结构 127.0.0.1 localhost === 192.168.1
(1)都定义在 netinet/in.h
(2)struct sockaddr,这个结构体是网络编程接口中用来表 示一个IP地址的,注意这个IP地址是不区分IPv4和IPv6的(或者说是兼容IPv4和IPv6的)
(3)typedef uint32_t in_addr_t; 网络内部用来表示IP地址的类型
(4)
1 2 3 4 5 struct in_addr { in_addr_t s_addr; };
(5)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <netinet/in.h> struct sockaddr { unsigned short sa_family; char sa_data[14 ]; }; struct sockaddr_in { short sin_family; unsigned short sin_port; struct in_addr sin_addr ; char sin_zero[8 ]; }; struct in_addr { unsigned long s_addr; };
(6)struct sockaddr 这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6所填充。
四、IP地址格式转换函数实践 1、网络通信中使用网络字节序,即大端模式 套接字:所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象 。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制 。从所处的地位来讲,套接字上连应用进程,下连网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议进行交互的接口 。套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。
套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定 。例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)
2、ipv4和ipv6的区别: (1)IPv6和IPv4都属于“互联网协议”。
(2)IPV6与IPV4的区别:
(3)IPv6的地址空间更大。
1)IPv4中规定IP地址长度为32,即有2^32-1个地址。
2)IPv6中IP地址的长度为128,即有2^128-1个地址。
(4)IPv6的路由表更小。
1)可使路由器能在路由表中,用一条记录表示一片子网。 2)大大减小了路由器中路由表的长度,提高了路由器转发数据包的速度。
(5)IPv6的组播支持以及对流的支持增强。
这使得网络上的多媒体应用有了长足发展的机会,为服务质量控制提供了良好的网络平台。
(6)IPv6加入了对自动配置的支持。
(7)IPv6具有更高的安全性。
在使用IPv6网络中,用户可以对网络层的数据进行加密并对IP报文进行校验,这极大地增强了网络安全。
(8)IPv6允许协议扩充。
(9)IPv6使用新的头部格式,简化和加速了路由选择过程,因为大多数的选项不需要由路由选择。
3、把ip地址转化为用于网络传输的二进制数值 1 2 3 4 5 6 7 8 int inet_aton (const char *cp, struct in_addr *inp) ;inet_aton() 转换网络主机地址ip(如192.168 .1 .10 )为二进制数值,并存储在struct in_addr 结构中,即第二个参数*inp ,函数返回非0表示cp 主机有地有效,返回0表示主机地址无效。(这个转换完后不能用于网络传输,还需要调用htons 或htonl 函数才能将主机字节顺序转化为网络字节顺序) in_addr_t inet_addr (const char *cp );inet_addr函数转换网络主机地址(如192.168 .1 .10 )为网络字节序二进制值,如果参数char *cp无效,函数返回-1 (INADDR_NONE),这个函数在处理地址为255.255 .255 .255 时也返回-1 ,255.255 .255 .255 是一个有效的地址,不过inet_addr无法处理;
4、将网络传输的二进制数值转化为成点分十进制的ip地址 1 2 3 4 char *inet_ntoa (struct in_addr in) ;inet_ntoa 函数转换网络字节排序的地址为标准的ASCII以点分开的地址,该函数返回指向点分开的字符串地址(如192.168 .1 .10 )的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(覆盖),所以如果需要保存该串最后复制出来自己管理!
我们如何输出一个点分十进制的IP呢?我们来看看下面的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> int main () { struct in_addr addr1,addr2; ulong l1,l2; l1= inet_addr("192.168.0.74" ); l2 = inet_addr("211.100.21.179" ); memcpy(&addr1, &l1, 4 ); memcpy(&addr2, &l2, 4 ); printf("%s : %s\n" , inet_ntoa(addr1), inet_ntoa(addr2)); printf("%s\n" , inet_ntoa(addr1)); printf("%s\n" , inet_ntoa(addr2)); return 0 ; } 实际运行结果如下: 192.168 .0 .74 : 192.168 .0 .74 192.168 .0 .74 211.100 .21 .179 inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配的,所以inet_ntoa后面的调用会覆盖上一次的调用。第一句printf的结果只能说明在printf里面的可变参数的求值是从右到左的,仅此而已。
5、新型网路地址转化函数inet_pton和inet_ntop
这两个函数是随IPv6出现的函数,对于IPv4地址和IPv6地址都适用,函数中p和n分别代表表达(presentation)和数值(numeric)。地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构的二进制值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 `#include <arpe/inet.h> int inet_pton (int family, const char *strptr, void *addrptr) ; 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。 inet_pton(AF_INET, ip, &foo.sin_addr); char str[INET_ADDRSTRLEN];char *ptr = inet_ntop(AF_INET,&foo.sin_addr, str, sizeof (str));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #define IP_ADDR "192.168.1.102" int main (int argc, char *argv[]) { #if 0 struct in_addr addr = {0 }; int ret = 0 ; char *addr1 = NULL ; printf ("IP address in decimal format:%s.\n" ,IP_ADDR); ret = inet_aton(IP_ADDR, &addr); if (ret != 0 ) { printf ("The address is in hex format:0x%x.\n" , addr.s_addr); } else { printf ("Invalid host address.\n" ); exit (-1 ); } if (NULL == addr1) { addr1 = inet_ntoa(addr); printf ("The dot decimal format is:%s.\n" , addr1); } else { printf ("Warning: This pointer has been referenced.\n" ); } #endif #if 0 in_addr_t ret = -1 ; printf ("IP address in decimal format:%s.\n" ,IP_ADDR); ret = inet_addr(IP_ADDR); if (ret == -1 ) { printf ("The passed parameter is invalid or\ the passed address cannot be processed.\n" ); exit (-1 ); } else { printf ("The address is in hex format:0x%x.\n" , ret); } #endif #if 1 int addr = 0 ; int ret = -1 ; char str[100 ] = {0 }; ret = inet_pton(AF_INET, IP_ADDR, &addr); if (ret != 1 ) { printf ("inet_pton error.\n" ); } else { printf ("The address is in hex format:0x%x.\n" , addr); } inet_ntop(AF_INET, &addr, str, sizeof (str)); printf ("IP address in decimal format:%s.\n" , str); #endif return 0 ; }
五、soekct实践编程 1、服务器端程序编写
(1)socket
(2)bind
(3)listen
(4)accept,返回值是一个fd,accept正确返回就表示我们已经和前来连接我的客户端之间建立了一个TCP连接了,以后我们就要通过这个连接来和客户端进行读写操作,读写操作就需要一个fd,这个fd就由accept来返回了。
注意:socket返回的fd叫做监听fd,是用来监听客户端的,不能用来和任何客户端进行读写;accept返回的fd叫做连接fd,用来和连接那端的客户端程序进行读写 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 `#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #define SERPORT 9003 #define SERADDR "192.168.1.67" #define BACKLOG 100 char recv_buf[100 ] = {0 };#define STAT_OK 30 #define STAT_ERR 31 typedef struct commu { char name[20 ]; int age; char cmd[20 ]; int stat; }info; int main (int argc, char *argv[]) { int sockfd = -1 , ret = -1 , clifd = -1 ; socklen_t len = 0 ; struct sockaddr_in seraddr = {0 }; struct sockaddr_in cliaddr = {0 }; char ipbuf[100 ] = {0 }; sockfd = socket(AF_INET, SOCK_STREAM, 0 ); if (-1 == sockfd) { perror("socket" ); return -1 ; } else { printf ("sockfd = %d.\n" , sockfd); } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(SERPORT); seraddr.sin_addr.s_addr = inet_addr(SERADDR); ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret < 0 ) { perror("bind" ); return 0 ; } else { ret = -1 ; printf ("bind success.\n" ); } ret = listen(sockfd, BACKLOG); if (ret < 0 ) { printf ("listen error.\n" ); } else { printf ("listen success" ); ret = -1 ; } clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len); printf ("链接已经建立, client_fd = %d.\n" , clifd); while (1 ) { info st1; int write_ret = -1 ; ret = recv(clifd, &st1, sizeof (info), 0 ); if (ret > 0 ) { printf ("数据包接收成功.\n" ); if (!strcmp ("login" , st1.cmd)) { printf ("客户端申请注册学生信息.\n" ); printf ("学生姓名:%s, 年龄:%d.\n" , st1.name, st1.age); int fd = open("a.txt" , O_WRONLY | O_APPEND | O_CREAT, 0666 ); if (fd < 0 ) { perror("打开文件失败" ); exit (-1 ); } else { printf ("打开文件成功\n" ); } write_ret = write(fd, &st1, sizeof (st1)); if (write_ret < 0 ) { perror("写入文件失败" ); exit (-1 ); } else { printf ("写入文件成功,写入字符个数为:%d\n" ,write_ret); st1.stat = STAT_OK; ret = send(clifd, &st1, sizeof (st1), 0 ); } } if (!strcmp ("check" , st1.cmd)) { } if (!strcmp ("getinfo" , st1.cmd)) { } } } return 0 ; }`
2、客户端程序编写
(1)socket
(2)connect 概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程 。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中 。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
IP地址用于确定某一台电脑,端口号用于确定某个进程(那个服务器)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 `#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <stdlib.h> #define SERPORT 9003 #define SERADDR "192.168.1.67" #define STAT_OK 30 #define STAT_ERR 31 char send_buf[100 ] = {0 };char recv_buf[100 ] = {0 }; typedef struct commu { char name[20 ]; int age; char cmd[20 ]; int stat; }info; int main (int argc, char *argv[]) { int sockfd = -1 , ret = -1 ; struct sockaddr_in seraddr = {0 }; struct sockaddr_in cliaddr = {0 }; sockfd = socket(AF_INET, SOCK_STREAM, 0 ); if (-1 == sockfd) { printf ("socket error.\n" ); exit (-1 ); } else { ret = -1 ; printf ("sockfd = %d.\n" , sockfd); } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(SERPORT); seraddr.sin_addr.s_addr = inet_addr(SERADDR); ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof (seraddr)); if (ret < 0 ) { printf ("connect error.\n" ); exit (-1 ); } else { printf ("connect successfully.\n" ); ret = -1 ; } #if 0 while (1 ) { printf ("请输入要发送的内容:.\n" ); scanf ("%s" , send_buf); ret = send(sockfd, send_buf, sizeof (send_buf), 0 ); if (ret < 0 ) { printf ("send error.\n" ); } else { ret = -1 ; printf ("发送了%d个字符.\n" , ret); } ret = recv(sockfd, send_buf, sizeof (send_buf), 0 ); if (ret < 0 ) { } else { ret = -1 ; printf ("成功接收了%d个字符.\n" , ret); printf ("client 发送的内容为:%s.\n" , send_buf); } } #endif printf ("访问远端服务器.\n" ); while (1 ) { info st1; char cmd[50 ] = {0 }; printf ("请输入学生的姓名:\n" ); scanf ("%s" , st1.name); printf ("请输入学生的年龄:\n" ); scanf ("%d" , &st1.age); printf ("请输入操作指令:\n" ); scanf ("%s" , st1.cmd); ret = send(sockfd, &st1, sizeof (st1), 0 ); memset (&st1, 0 , sizeof (st1)); ret = recv(sockfd, &st1, sizeof (st1), 0 ); if (ret > 0 ) { printf ("收到回复.\n" ); if (!strcmp ("login" , st1.cmd)) { if (st1.stat == STAT_OK) { printf ("学生信息注册成功.\n" ); } else { printf ("学生注册信息失败.\n" ); } } if (!strcmp ("check" , st1.cmd)) { if (st1.stat == STAT_OK) { printf ("学生信息检查成功.\n" ); } else { printf ("学生信息检查失败.\n" ); } } if (!strcmp ("getinfo" , st1.cmd)) { if (st1.stat == STAT_OK) { printf ("学生信息获取成功.\n" ); } else { printf ("学生信息获取失败.\n" ); } } } } return 0 ; }
3、客户端发送&服务器接收
4、服务器发送&客户端接收
上述程序实际效果:
5、探讨:如何让服务器和客户端好好沟通
(1)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收。
(2)必须了解到的一点:client和server之间的通信是异步的,这就是问题的根源
(3)解决方案:依靠应用层协议来解决。说白了就是我们server和client事先做好一系列的通信约定。
6、自定义应用层协议第一步:规定发送和接收方法
7、UDP简介
无连接协议,也称透明协议,也位于传输层。
两者区别:
1) TCP提供面向连接的传输,通信前要先建立连接(三次握手机制); UDP提供无连接的传输,通信前不需要建立连接 。 2) TCP提供可靠的传输(有序,无差错,不丢失,不重复); UDP提供不可靠的传输 。 3) TCP面向字节流的传输,因此它能将信息分割成组,并在接收端将其重组; UDP是面向数据报的传输,没有分组开销 。 4) TCP提供拥塞控制和流量控制机制; UDP不提供拥塞控制和流量控制机制 。
==至此,本阶段学习就结束了,希望对大家入门Linux IO和网络编程有所帮助! ==
注: (引用自 https://blog.csdn.net/weixin_45842280/article/details/120091040 )