1、TCP/IP协议概述

1.1、OSI参考模型及TCP/IP参考模型

OSI协议参考模型是基于国际标准化组织(ISO)的建议发展起来的,从上到下工分为7层:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层。与此相区别的TCP/IP协议模型一开始就遵循简单明确的设计思路,它将OSI的7层参考模型简化为4层,从而得到有利于实现和使用。TCP/IP协议参考模型和OSI协议参考模型的对应关系如下图所示:
网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。要注意的是数据帧是独立的网络星系传输单元。
网络层:负责将数据帧封装成IP数据报,并运行必要的路由算法。
传输层:负责端对端之间的通信会话连接和简历,传输协议的选择根据数据传输方式而定。
应用层:负责应用层序的网络访问,这里通过端口来识别各个不同的进程。

1.2、TCP/IP协议族

TCP/IP是一个庞大的协议族,它包括了各个层次上的众多协议,下图列举了各层只能怪一些重要的协议,并给出了协议在不同层次中处的位置:
ARP:用于获得同一物理网络中的硬件主机地址
MPLS:多协议标签协议。
IP:负责在主机和网络之间寻址和路由数据包。
ICMP:用于发送报告有关数据包的传送错误的协议;
IGMP:被IP主机用来想本地多路广播路由器报告主机组成员的协议;
TCP:为应用程序提供可靠的通信连接,适合于一次传输大批数据的情况,并适用于要求得到响应的应用程序。
UDP:提供了无连接通信,且不对传递包进行可靠的保证,适合于一次传输少量的数据,可靠性则由应用层来负责。

1.3、TCP和UDP

1、TCP
(1)概述
同其他特任何协议一样,TCP想相邻的高层提供服务,因为TCP的上一层就是应用层,因此,TCP数据传输实现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用TCP并使用TCP服务,提供需要准备发送的数据,用来区分接收数据应用的目的地址和端口号。
通常应用程序通过打开一个socket来使用TCP服务,TCP管理到其他socket的数据传递。可以说,通过IP的源/目的可以唯一的区分网络中两个设备的关联。
(2)三次握手协议
TCP对话通过三次握手来初始化的。三次握手的目的是使数据段的发送和接搜同步,告诉其他主机其一次可接搜的数据量,并建立虚链接。
三次握手的简单过程:
1、初始化主机通过一个同步标志置位的数据段发出会话请求;
2、接收主机通过发回具有以下项目的数据段表示回复:同步标志置位、即将发送的数据段的起始字节的顺序号、应答并带有将收到的下一个数据段的字节顺序号。
3、请求主机再回一个数据段,并带有确认顺序号和确认号。
流程示意图如下:
TCP实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报到达目的地后,接收方的TCP实体向回发送一个数据报,其中包含一个确认序号,它的意思是希望收到下一个数据报的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据报。
(3)TCP数据报头
TCP数据报头的含义如下:
源端口、目的端口:16位长。标识出远端和本地的端口号。
序号:32位长。标识发送的数据报的顺序。
确认号:32位长。希望收到的下一个数据报的序列号。
TCP头长:4位长。表明TCP头中包含多少个32位字。
6位未用。
ACK:ACK位置1表明确认号是合法的。如果ACK为0,那么数据报不包含确认信息,确认字段被省略。
PSH:表示是带有PUSH标志的数据,接收方因此请求数据报一到便可送往应用程序而不必等到缓冲区装满时才传送。
RST:用于复位由于主机崩溃或其他原因而出现的错误链接。还可以用于拒绝非法的数据报或拒绝连接请求。
SYN:用于建立连接
FIN:用于释放连接。
窗口大小:16位长。窗口大小字段表示在确认了字节之后还可以发送多少个字节。
校验和:16位长。是为了确保高可靠性而设置的。它的检验头部、数据伪TCP头部之和。
可选项:0个或多个32位字。包括最大TCP载荷,窗口比例、选择重发数据报等选项。

2、UDP

UDP协议并不需要建立一个明确的连接,因此建立UDP应用要比建立TCP应用简单得多。
UDP数据包头如下图所示:
源地址、目的地址、:16位长。标识远端和本地的端口号。
数据报的长度是指包括报头和数据部分在内的总的字节数,因为报头的长度是固定的。所以该域主要用来计算可变长度的数据部分(又称数据负载)

3、协议的选择

协议的选择要考虑三个方面:
(1)对数据可靠性的要求
对数据要求高可靠性的应用需要选择TCP协议,如验证、密码字段的传送都是不允许出错的,而对数据的可靠性要求不那么高的应用科选择UDP传送。
(2)应用的实时性
由于TCP协议在传送过程中要进行三次握手、重传确认等手段来保证数据传输的可靠性。使用TCP协议会有较大的延时。因此不适合对实时性较高的应用。如VOIP、视频监控等。相反,UDP协议则在这些应用中能发挥很好的作用。
(3)网络的可靠性
在网络状况不是很好的情况下需选用TCP协议(如广域网等情况),但是若在网络状况很好的情况下(如局域网)选择UDP协议来减少网络负荷。

4、网络基础编程

4.1、socket概述

1、socket定义
在Linux中的网络编程都是通过stocket接口来完成的。socket接口是一种特殊的I/O接口,它也是一种文件描述符。每一个socket都用一个半相关描述{协议,本地地址,本地端口}来表示;一个完整的套接字则用一个相关描述{协议,本地地址、本地端口、远程地址、远程端口}。socket也有一个类似打开文件的函数调用,该函数返回一个整型的socket描述符,随后的连接、数据传输等操作都是通过socket来实现的。
2、socket类型
常见的socket类型有3种:
(1)流式socket(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
(2)数据报socket(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文传输,是无序的。并且不保证是可靠的、无差错的。它使用数据报协议UDP。
(3)原始socket
原始套接字允许对底层协议如IP或IGMP进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
 
2.2、地址及顺序处理
1、地址结构相关处理
(1)数据结构介绍
sockaddr和sockaddr_in两个结构体用来保存socket的信息,如下所示:
struct sockaddr {
unsigned short sa_family;/*地址族*/
char sa_data[14];/*14字节的协议地址,包含该socket的IP地址和端口*/
}; struct sockaddr_in {
short nt sa_family;/*地址族*/
unsigned short int sin_port;/*端口号*/
struct in_addr sin_addr;/*IP地址*/
unsigned char sin_zero[8];/*填充0以保持与struct sockaddr同样大小*/
};
(2)结构字段

结构头文件 #include
sa_family AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX域协议
AF_LINK:链路地址协议
AF_KEY:密钥套接字(socket)
 

2、数据存储优先顺序
(1)函数说明
计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。Internet上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这这两个字节存储优先吮吸进行相互转化。四个函数:htons、ntohs、htonl、ntohl。
(2)函数格式
所需头文件
#include
函数原型
uint16_t htons(uint16_t host16bit)
uint32_t htonl(uint16_t host32bit)
uint16_t ntohs(uint16_t net16bit)
uint16_t ntohl(uint16_t net32bit)
参数 host16bit:主机字节序的16bit数据
host32bit:主机字节序的32bit数据
net16bit:网络字节序的16bit数据
net32bit:网络字节序的32bit数据
返回值 成功:返回要转换的字节
失败:-1
 
        3、地址格式化转化
(1)函数说明
通常用户在表达地址时采用点分十进制的数值。而通常使用的socket编程中使用的则是二进制,这就需要将这两个数值进行转换。这里IPv4用到的函数有:inet_aton、inet_addr、和inet_ntoa,而IPv4和IPv6兼容的函数有inet_pton(点分十进制地址映射为二进制地址)和inet_ntop(二进制地址映射为点分十进制地址)。
 
(2)函数格式
 
inet_pton函数:
所需头文件 #include
函数原型 int inet_pton(int family,const char *strptr, void *addrptr)
参数 family: AF_INET:IPv4协议;AF_INET6:IPv6协议
strptr:要转化的值
addrptr:转化后的地址
返回值 成功:0
失败:-1
 
    inet_ntop函数:
所需头文件 #include
函数原型 int inet_ntop(int family,void *addrptr, char *strptr, size_t len)
参数 family: AF_INET:IPv4协议;AF_INET6:IPv6协议
strptr:要转化的值
addrptr:转化后的地址;len:转化后值得大小
返回值 成功:0
失败:-1
 
4、名字地址转化:
(1)函数说明
在Linux中,有一些函数可以实现主机名和地址的转化,最为常见的有:gethostbyname(主机名转化为IP)、gethostbyaddr(IP地址转化无主机名)、getaddrinfo(自动识别IPv4地址和IPv6地址)等。
gethostbyname(主机名转化为IP)和gethostbyaddr(IP地址转化无主机名)都涉及到一个hostent结构体:
struct hostent {
char *h_name;/*正式主机名*/
char **h_aliases;/*主机别名*/
int h_addrtype;/*地址类型*/
int h_length;/*地址长度*/
char **h_addr_list;/*指向IPv4或IPv6的地址指针数组*/
};调用该函数后就能返回hostent结构体的相关信息。
getaddrinfo函数涉及到一个addrinfo结构体,如下:
struct addrinfo{
int ai_flags;/*AI_PASSIVE,A_CANONNAME*/
int ai_family;/*地址族*/
int ai_socktype;/*socket类型*/
int ai_protocol;/*协议类型*/
size_t ai_addrlen;/*地址长度*/
char *ai_canoname;/*主机名*/
struct sockaddr *ai_addr;/socket结构体/
struct addrinfo *ai_next;/*下一个指针链表*/
}
gethostbyname函数:
所需头文件 #include
函数原型 struct hostent *gethostbyname(const char *hostname)
参数 hostname :主机名
返回值 成功:hostent类型指针
失败:-1
调用该函数时可以先对addrinfo结构体中的h_addrtype和h_length进行设置若为Ipv4可设置为AF_INET和4,若为IPv6可设置为AF_INET6和16,如不设置则默认为IPv4地址类型。
 
getaddrinfo函数:

头文件 #include
函数原型 int getaddringo(const *hostname,const char *service,const struct addrinfo *hints,struct addrinfo **result)
参数 hostname:主机名
service:服务名或十进制的串口字符串
hints:服务线索
result:返回结果
返回值 成功:0
失败:-1
在调用前,首先对hints服务线索进行设置。它是一个addrinfo结构体
addrinfo结构体常见选项值
结构体头文件  
ai_flags AI_PASSIVE:该套接口是用作被动的打开
AI_CANONNAME:通知getaddrinfo函数返回主机名字
family AF_INET:IPv协议
AF_INET6:IPv6协议
AF_UNSPE:IPv4或IPv6均可:
ai_socktype SOCK_STREAM:字节流套接字socket(TCP)
SOCK_DGRAM:数据报套接字socket(UDP)
ai_protocol IPPROTO_IP:IP协议
IPPROTO_IPV4:IPv4协议
IPPROTO_IPV6:IPv6协议
IPPROTO_UDP:UDP协议
IPPROTO_TCP:TCP协议
 
(3)使用实例:
/*getaddrinfo.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h> int main()
{
struct addrinfo hints, *res = NULL; int rc;
memset(&hints,0,sizeof(hints)); /*设置addrinfo结构体各参数*/ hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP; /*调用getaddrinfo函数*/ rc = getaddrinfo("127.0.0.1","123",&hints,&res);
if(rc != 0)
{
perror("getaddrinfo");
exit(1);
} else
printf("getaddrinfo success\n"); }

4.3、socket基础编程

(1)函数说明
进行socket编程的基本函数有socket、bind、listen、accept、send、sendto、recv、recvform这几个;
socket:该函数用于建立一个socket连接,可指定socket类型等信息,在建立socket连接之后可对sockaddr或sockaddr_in进行初始化,以保存所建立的socket信息。
bind:该函数用于将本地IP地址绑定端口号,若绑定其他地址则不成功,另外,它主要用于TCP的连接,而在UDP中的连接中无必要。
connect:该函数在TCP中是用于bind的之后的client端,用于与服务器建立连接,而在UDP中由于没有了bind函数,因此connect有点类似bind函数的作用。
send和recv:这两个函数用于接收和发送数据,可一再TCP中,也可以在UDP中。当用在UDP时,可以在connect函数建立连接之后再用。
sendto和recvfrom:这两个函数的作用同send和recv函数类似,。当用在TCP时后面几个与地址有关的参数起不了作用。函作用等同于send和recv;当用在UDP时,可以在之前没有使用connect的情况时,这两个函数可以自动寻找指定地址进行连接。
 
服务器和客服端使用TCP协议的流程如如下图:
服务器和客户端使用UDP协议的流程图如下图:
(2)函数格式
socket函数
所需头文件 #include
函数原型 int socket(int family, int type, int protocol)
参数
family:
协议族
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX域协议
AF_ROUTE:路由套接字
AF_KEY:密钥套接字
type:
套接字类型
SOCK_STREAM:字节流套接字
SOCK_DGRAM:数据报套接字
SOCK_RAW:原始套接字
protocol:0(原始套接字除外)
返回值 成功:非负套接字描述符
失败:-1
bind函数:
所需头文件 #include
函数原型 int bind(int sockfd,struct sockaddr *my_addr, int addrlen)
参数原型 sockfd:套接字描述符
my_addr:本地地址
addrlen:地址长度
返回值 成功:0
失败:-1
端口号和地址在my_addr中给出了,若不指定地址,则内核随意分配一个临时端口给该应用程序。
 
listen函数:
所需头文件 #include
函数原型 int listen(int socket, int backlog)
参数原型 sockfd:套接字描述符
backlog:请求队列中允许的最大请求数,大多数系统缺省值为20
返回值 成功:0
失败:-1
 
accept函数:
所需头文件 #include
函数原型 int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
参数 sockfd:套接字描述符
addr:客户端地址
addrlen:地址长度
返回值 成功:0
失败:-1
 
connect函数:
所需头文件 #include
函数原型 int connect(int sockfd,struct sockaddr *serv_addr,int addrlen)
参数 sockfd:套接字描述符
serv_add:服务器地址
addrlen:地址长度
返回值 成功:0
失败:-1
 
send函数:
所需头文件 #include
函数原型 int send(int sockfd, const void *msg, int len, int flags)
参数 sockfd:套接字描述符
msg:指向发送数据的指针
len:数据长度
flags:一般为0
返回值 成功:发送的字节数
失败:-1
 
recv函数:
所需头文件 #include
函数原型 int recv(int sockfd, void *buf, int len, unsigned int flags)
参数
sockfd:套接字 描述符
buf:存放接收数据的缓冲区
len:数据长度
flags:一般为0
返回值 成功:接收的字节数
失败:-1
 
sendo函数:
所需头文件 #include
函数原型 int sendto(int sockfd, const void *msg,int len ,unsigned int flags,const struct sockaddr *to, int tolen)
参数 soctfd:套接字描述符
msg:指向要发送数据的指针
len:数据长度
flags:一般为0
to:目的机的IP地址和端口信息
tolen:地址长度
返回值 成功:发送的字节数
失败:-1
 
recvfrom函数:
所需头文件 #include
函数原型 int recvfrom(int sockfd, void *buf,int len, unsigned int flags,struct sockaddr *from,int *fromlen)
参数 sockfd:套接字描述符
buf:存放接收数据的缓冲区
len:数据长度
flags:一般为0
from:源机的IP地址和端口号信息
fromto:地址长度
返回值 成功:接收的字节数
失败:-1
 
(3)使用实例
/*server.c*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define SERVPORT 3333
#define BACKLOG 10
#define MAX_CONNECTED_NO 10
#define MAXDATASIZE 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd,client_fd;
char buf[MAXDATASIZE];
/*建立socket连接*/
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1){
perror("socket");
exit(1);
}
printf("socket success! sockfd = %d\n", sockfd);
/*设置sockaddr_in结构体相关参数*/
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(SERVPORT);
server_sockaddr.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_sockaddr.sin_zero),8);
/*绑定函数bind*/
if(bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
printf("bind success!\n");
/*调用listen函数*/
if(listen(sockfd,BACKLOG) == -1)
{
perror("listen");
exit(1);
}
printf("listening...\n");
/*调用accept函数*/
if((client_fd = accept(sockfd,(struct sockaddr *)&client_sockaddr,&sin_size)) == -1)
{
perror("accept");
exit(1);
}
/*调用recv函数接收客户端的请求*/
if((recvbytes = recv(client_fd,buf,MAXDATASIZE,0)) == -1)
{
perror("recv");
exit(1);
}
printf("received a connection : %s\n",buf);
close(sockfd);
}
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char **argv)
{
int sockfd,sendbytes;
char buf[MAXDATASIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if(argc < 2)
{
fprintf(stderr,"Please enter the sercer's hostname!\n");
exit(1);
}
/*地址解析函数*/
if((host = (struct hostent *)gethostname(argv[1]))==NULL){
perror("gethostname");
exit(1);
}
/*创建socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM,0)) == -1)
{
perror("socket");
exit(1);
}
/*设置sockaddr_in结构体相关参数*/
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
serv_addr.sin_addr=*((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
/*调用connect函数主动发起对服务端的连接*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(1);
}
/*发送消息给服务器*/
if((sendbytes = send(sockfd,"hello",5,0)) == -1){
perror("send");
exit(1);
}
close(sockfd);
}

  

Linux应用程序设计之网络基础编程的更多相关文章

  1. linux驱动程序设计的硬件基础,王明学learn

    linux驱动程序设计的硬件基础(一) 本章讲总结学习linux设备程序设计的硬件基础. 一.处理器 1.1通用处理器 通用处理器(GPP)并不针对特定的应用领域进行体系结构和指令集的优化,它们具有一 ...

  2. [Linux]经典面试题 - 网络基础 - TCP三次握手

    [Linux]经典面试题 - 网络基础 - TCP三次握手 目录 [Linux]经典面试题 - 网络基础 - TCP三次握手 一.TCP报文格式 1.1 TCP报头 1.2 报文图例 二.TCP三次握 ...

  3. 【苏勇老师Linux 入门笔记】网络基础

    IP 地址 IP 编制时一个双层编制方案,一个 IP 地址标示一个主机 (或一个网卡接口). 一个 IP 地址分为两个部分:网络部分(所属区域)和主机部分(标示区域中的哪个主机).IPv4 共32位, ...

  4. C#网络程序设计(2)Socket基础编程

        本节介绍如何使用基础Socket实现TCP通信.     (1)Socket详细介绍: Socket的英文原义是"孔"或"插座".通常称作"套 ...

  5. 【网络基础编程】第三节 C/S

    学习地址: C语言中文网 - 实现迭代服务端和客户端 GNU - Closing a Socket 前面介绍的程序,不管Service 端还是 Client端,都有一个问题,就是处理完一个 accep ...

  6. 网络基础编程_5.4聊天室-IOCP服务器

    聊天室-IOCP服务器 main 创建完成端口内核对象(CreateIoCompletionPort) 获取核心数并创建线程(GetSystemInfo + CreateThread) 创建套接字并绑 ...

  7. linux需要了解的网络基础知识

    第1章 网络命令 1.1 用户模式下的命令 1.1.1 enable切换到特权模式 Router>enable Router# 特权模式 Router# Router# 1.2 特权模式下的命令 ...

  8. 7)Linux程序设计入门--网络编程

    )Linux程序设计入门--网络编程 Linux系统的一个主要特点是他的网络功能非常强大.随着网络的日益普及,基于网络的 应用也将越来越多. 在这个网络时代,掌握了Linux的网络编程技术,将令每一个 ...

  9. linux服务器开发三(网络编程)

    网络基础 协议的概念 什么是协议 从应用的角度出发,协议可理解为"规则",是数据传输和数据的解释的规则. 假设,A.B双方欲传输文件.规定: 第一次,传输文件名,接收方接收到文件名 ...

随机推荐

  1. +new Date()的用法

    var s=+newDate();   var s=+newDate(); 解释如下:=+是不存在的; +new Date()是一个东西; +相当于.valueOf(); 看到回复补充一下.getTi ...

  2. LeetCode 657. Robot Return to Origin (C++)

    题目: There is a robot starting at position (0, 0), the origin, on a 2D plane. Given a sequence of its ...

  3. Answer the questions(回答自己的问题)

    第一章: 问题:我们现在学了这个专业,如果想全面去了解,应该还要学习哪些课程? 回答:其实软件工程只是一个比较大的范畴,以后如果要出去工作,我们还要细分,比如说开发安卓,开发游戏,web架构方面等很多 ...

  4. iOS开发面试题(中级)

    //想面试的童鞋们来看看自己会多少, 老鸟可以无视直接绕过...1. Object-c的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是分类好?为什么?与Ex ...

  5. Scrum 项目 4.0-5.0-约教网站开发(一)

    ----------------------------------4.0----------------------------------------------- 一.项目任务 1.准备看板. ...

  6. 清除浮动小记,兼容Ie6,7

    .clearfix { *zoom:1;} .clearfix:after{clear:both; display:block; height:0; visibility:hidden; line-h ...

  7. PHP数据库常用常量笔记

    参考:http://php.net/manual/zh/pdo.constants.php Warning 自 PHP 5.1 起,开始使用类常量.以前的版本使用类似 PDO_PARAM_BOOL 这 ...

  8. [转帖]USB-C和Thunderbolt 3连接线你搞懂了吗?---没搞明白.

    USB-C和Thunderbolt 3连接线你搞懂了吗? 2018年11月25日 07:30 6318 次阅读 稿源:威锋网 3 条评论 按照计算行业的风潮,USB Type-C 将会是下一代主流的接 ...

  9. Opera官网打不开 下载Opera最新版本的实际地址

    目前Opera官网可以打开,但是点下载时就会出错,国内无法访问Opera的下载地址,无法通过官网直接下载Opera浏览器.下面提供下载的方式. 一.通过官方的ftp站点下载 FTP地址为 http:/ ...

  10. #46 delete(动态规划+树状数组)

    二维的dp非常显然,但这也没有什么优化的余地了. 注意到最后的方案中只有产生贡献的位置是有用的,剩下的部分可以在该范围内任意选取. 所以我们考虑设f[i]为i号位最后产生贡献的答案,则f[i]=max ...