Linux网络编程实践

Linux网络编程实践

[TOC]

一、Linux网络编程框架

1、网络是分层的

(1)OSI 7层模型

OSI 7层模型

(2)网络为什么要分层

1)各层次之间是独立的。某一层并不需要知道它的下一层是如何实现的,而仅仅需要知道该层通过层间的接口所提供的服务。这样,整个问题的复杂程度就下降了。也就是说上一层的工作如何进行并不影响下一层的工作,这样我们在进行每一层的工作设计时只要保证接口不变可以随意调整层内的工作方式。

2)灵活性好。当任何一层发生变化时,只要层间接口关系保持不变,则在这层以上或以下层均不受影响。当某一层出现技术革新或者某一层在工作中出现问题时不会连累到其它层的工作,排除问题时也只需要考虑这一层单独的问题即可。

3)结构上可分割开。各层都可以采用最合适的技术来实现。技术的发展往往不对称的,层次化的划分有效避免了木桶效应,不会因为某一方面技术的不完善而影响整体的工作效率。

4)易于实现和维护。这种结构使得实现和调试一个庞大又复杂的系统变得易于处理,因为整个的系统已经被分解为若干个相对独立的子系统。进行调试和维护时,可以对每一层进行单独的调试,避免了出现找不到、解决错问题的情况。

  1. 能促进标准化工作。因为每一层的功能及其所提供的服务都已有了精确的说明。标准化的好处就是可以随意替换其中的某一层,对于使用和科研来说十分方便。

(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>          /* See NOTES */
#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>          /* See NOTES */
#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>          /* See NOTES */
#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>          /* See NOTES */
#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); //返回连接connect_fd

参数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; // 2 bytes address family, AF_xxx
char sa_data[14]; // 14 bytes of protocol address
};

// IPv4 AF_INET sockets:

struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};

struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};

(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表示主机地址无效。(这个转换完后不能用于网络传输,还需要调用htonshtonl函数才能将主机字节顺序转化为网络字节顺序)

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 //从这里可以看出,printf里的inet_ntoa只运行了一次。  

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);
//将点分十进制的ip地址转化为用于网络传输的数值格式
//返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1

const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
//将数值格式转化为点分十进制的ip地址格式
//返回值:若成功则为指向结构的指针,若出错则为NULL
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);
//代替 foo.sin_addr.s_addr=inet_addr(ip);

char str[INET_ADDRSTRLEN];
char *ptr = inet_ntop(AF_INET,&foo.sin_addr, str, sizeof(str));
// 代替 ptr = inet_ntoa(foo.sin_addr)
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;

/*Error in this statement. This macro definition will not be replaced*/
//printf("IP address in decimal format:IP_ADDR.\n");/*error*/
printf("IP address in decimal format:%s.\n",IP_ADDR);/*correct*/

//printf("IP address in decimal format:IP_ADDR.\n");
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 //服务器开放给我们的IP地址和端口号
#define SERADDR "192.168.1.67"
#define BACKLOG 100

char recv_buf[100] = {0};

#define STAT_OK 30 // 回复ok
#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};


//第一步:使用socket打开文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("socket");
return -1;
}
else
{
printf("sockfd = %d.\n", sockfd);
}

//第二步:bind绑定sockfd和当前电脑的ip地址以及端口号
seraddr.sin_family = AF_INET; // 设置地址族为IPv4;
seraddr.sin_port = htons(SERPORT); //设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR);//设置IP地址
ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
if (ret < 0)
{
perror("bind");
return 0;
}
else
{
ret = -1;
printf("bind success.\n");
}

//第三步,使用listen监听端口
ret = listen(sockfd, BACKLOG);
if (ret < 0)
{
printf("listen error.\n");

}
else
{
printf("listen success");
ret = -1;
}

//第四步:accept阻塞等待客户端接入
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);//返回连接connect_fd文件描述符
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");

//对于cmd命令回复的处理
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)
{
//printf("打开文件失败\n");
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);
}


}

//对于check命令的处理
if (!strcmp("check", st1.cmd))
{
//可自己设计功能实现

}

//对于getinfo命令的处理
if (!strcmp("getinfo", st1.cmd))
{
//可自己设计功能实现
}

}//end ret > 0

}//end while(1)

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 //服务器开放给我们的IP地址和端口号
#define SERADDR "192.168.1.67"

#define STAT_OK 30 // 回复ok
#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};//使用IPV4
struct sockaddr_in cliaddr = {0};

//第一步:使用socket打开文件描述符
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
printf("socket error.\n");
exit(-1);
}
else
{
ret = -1;
printf("sockfd = %d.\n", sockfd);
}

//第二步:connect链接服务器,发起三次
seraddr.sin_family = AF_INET; // 设置地址族为IPv4;
seraddr.sin_port = htons(SERPORT); //设置地址的端口号信息
seraddr.sin_addr.s_addr = inet_addr(SERADDR);//设置IP地址

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;
}
//第三步:建立连接后就可以开始通信了
//strcpy(sendbuf, "hello world.");
//ret = send(sockfd, send_buf, sizeof(send_buf), 0);
//printf("发送了%d个字节.\n", ret);
#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)
{
//printf("receive error.\n");

}
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");

//对于cmd命令回复的处理
if (!strcmp("login", st1.cmd))
{
if (st1.stat == STAT_OK)
{
printf("学生信息注册成功.\n");
}
else
{
printf("学生注册信息失败.\n");
}

}

//对于check命令的处理
if (!strcmp("check", st1.cmd))
{
if (st1.stat == STAT_OK)
{
printf("学生信息检查成功.\n");

}
else
{
printf("学生信息检查失败.\n");
}

}

//对于getinfo命令的处理
if (!strcmp("getinfo", st1.cmd))
{
if (st1.stat == STAT_OK)
{
printf("学生信息获取成功.\n");

}
else
{
printf("学生信息获取失败.\n");
}

}

}//end ret >0

}//end while(1)

return 0;
}

3、客户端发送&服务器接收

4、服务器发送&客户端接收

上述程序实际效果:

img

5、探讨:如何让服务器和客户端好好沟通

  • (1)客户端和服务器原则上都可以任意的发和收,但是实际上双方必须配合:client发的时候server就收,而server发的时候client就收。

  • (2)必须了解到的一点:client和server之间的通信是异步的,这就是问题的根源

  • (3)解决方案:依靠应用层协议来解决。说白了就是我们server和client事先做好一系列的通信约定。

6、自定义应用层协议第一步:规定发送和接收方法

  • (1)规定连接建立后由客户端主动向服务器发出1个请求数据包,然后服务器收到数据包后回复客户端一个回应数据包,这就是一个通信回合

  • (2)整个连接的通信就是由N多个回合组成的。

  • (3)自定义应用层协议第二步:定义数据包格式

    常用应用层协议:http、ftp······

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)