Chinaunix首页 | 论坛 | 博客
  • 博客访问: 291127
  • 博文数量: 52
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 587
  • 用 户 组: 普通用户
  • 注册时间: 2017-03-09 09:24
个人简介

水滴

文章分类

全部博文(52)

文章存档

2021年(3)

2019年(8)

2018年(32)

2017年(9)

我的朋友

分类: LINUX

2017-04-19 10:12:24

1 简介

TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP,TCP/IP 是互联网相关的各类协议族的总称,比如:TCP,UDP,IP,FTP,HTTP,ICMP,SMTP 等都属于 TCP/IP 族内的协议。
TCP/IP模型是互联网的基础,它是一系列网络协议的总称。这些协议可以划分为四层,分别为链路层、网络层、传输层和应用层。


  • 链路层:负责封装和解封装IP报文,发送和接受ARP/RARP报文等。
  • 网络层:负责路由以及把分组报文发送给目标网络或主机。
  • 传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。
  • 应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。


1.1 udp

UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
它有以下几个特点:

1.1.1 面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:


  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作


1.1.2 有单播,多播,广播的功能

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

1.1.3 UDP是面向报文的

发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文

1.1.4 不可靠性

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

1.1.5 头部开销小,传输数据报文时是很高效的。

UDP 头部包含了以下几个数据:


  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口
  • 整个数据报文的长度
  • 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误


因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的

1.2 TCP

TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构。
因此在处理TCP数据时,则需要根据协议来完成报文的解析,已防粘报问题。

1.2.1 TCP连接过程

建立一个TCP连接需要三次握手:


  1. 第一次握手


客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态。


  1. 第二次握手


服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态。


  1. 第三次握手


当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
为什么 TCP 建立连接需要三次握手,而不是两次?这是因为这是为了防止出现失效的连接请求报文段被服务端接收的情况,从而产生错误。

1.2.2 TCP断开链接

TCP 是全双工的,在断开连接时两端都需要发送 FIN 和 ACK。


  1. 第一次握手


若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。


  1. 第二次握手


B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明 A 到 B 的连接已经释放,不再接收 A 发的数据了。但是因为 TCP 连接是双向的,所以 B 仍旧可以发送数据给 A。


  1. 第三次握手


B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
第四次握手
A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。

1.2.3 TCP协议的特点


  • 面向连接


面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。


  • 仅支持单播传输


每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。


  • 面向字节流


TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。


  • 可靠传输


对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。


  • 提供拥塞控制


当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞


  • TCP提供全双工通信


TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)

1.3 IO复用

I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll都是IO多路复用的机制。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。下来,分别谈谈。

1.3.1 select

select 的核心功能是调用tcp文件系统的poll函数,不停的查询,如果没有想要的数据,主动执行一次调度(防止一直占用cpu),直到有一个连接有想要的消息为止。从这里可以看出select的执行方式基本就是不同的调用poll,直到有需要的消息为止。
缺点:
1、每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
2、同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
3、select支持的文件描述符数量太小了,默认是1024。
优点:
1、select的可移植性更好,在某些Unix系统上不支持poll()。
2、select对于超时值提供了更好的精度:微秒,而poll是毫秒。

1.3.2 poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
缺点:
1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;
2、与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
优点:
1、poll() 不要求开发者计算最大文件描述符加一的大小。
2、poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
3、它没有最大连接数的限制,原因是它是基于链表来存储的。

1.3.3 epoll

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一 个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射技术,这 样便彻底省掉了这些文件描述符在系统调用时复制的开销。
epoll的优点就是改进了前面所说缺点:
1、支持一个进程打开大数目的socket描述符:相比select,epoll则没有对FD的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2、IO效率不随FD数目增加而线性下降:epoll不存在这个问题,它只会对"活跃"的socket进行操作--- 这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3、使用mmap加速内核与用户空间的消息传递:这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

1.3.4 三者对比与区别:

1、select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
2、select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

2 代码实例

常用的IO多路复用,有select和epoll, 这里 udp方式采用select, tcp采用epoll;

2.1 UDP

int udp_init(short port char *ip)
{
int rc;
int udpsockfd = -1;
struct sockaddr_in sa;
udpsockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(udpsockfd < 0)
{
return -1;
}
sa.sin_family = AF_INET;
//sa.sin_addr.s_addr = htonl(INADDR_ANY); //任意地址 多用于服务器端
sa.sin_addr.s_addr = inet_addr(ip)
sa.sin_port = htons(port);
/*客户端可以不进行绑定设置*/
if(bind(udpsockfd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
{
close(udpsockfd);
return -1;
}
rc = 1;
if(setsockopt(udpsockfd, SOL_SOCKET, SO_REUSEADDR, (char *)&rc, sizeof(rc)) < 0)
{
close(fd);
return -1;
}
if(fcntl(udpsockfd, F_SETFL, fcntl(udpsockfd, F_GETFL, 0) | O_NONBLOCK) < 0)
{
close(udpsockfd);
return -1;
}
return udpsockfd;
}
int udp_send(int fd, char *ipaddr, short port, char *buf, int length)
{
struct sockaddr_in sa;
if((fd < 0) || (NULL==ipaddr)||(NULL == buf)
{
return -1;
}
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = inet_addr(ipaddr);
sa.sin_port = htons(port);
if(sendto(fd, (void *)buf, length, 0, (struct sockaddr *)(&sa), sizeof(struct sockaddr_in)) != length)
{
return -1;
}
return 0;
}
int udp_recv(int fd)
{
fs_set readfds;
int nfds = -1;
struct sockaddr_in from;
int from_len = 0;
struct timeval timeout;
char readbuf[1024] = {0};
int numbytes = 0;
while(1)
{
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 3;
timeout.tv_usec = 0;
nfds= fd + 1;
nfds = select(fds, &readfds, NULL, NULL, &timeout);
if(nfds < 0)
{
break;
}
else if(nfds == 0)
{
// timeout
continue;
}
else
{
if(FD_ISSET(fd, &nfds))
{
memset(readbuf, 0 , sizeof(readbuf));
numbytes = recvfrom(fd, readbuf, sizeof(readbuf), (struct sockaddr *)&from, (socklen_t *) &from_len);
if(numbytes)
{
//process
}
}
}
}
}

2.1 TCP

2.1.1 client

static int tcp_connect(int flag, const char *ser_ip, const char *ser_port)
{
int ret = 0;
int flags;
struct hostent *he;
struct sockaddr_in caster;
char *b;
long port;
int sockfd;
static int err_no = 0;
if(ser_ip == NULL || ser_port == NULL)
{
return -1;
}
if(!(he = gethostbyname(ser_ip)))
{
return -1;
}
else
{
err_no = 0;
}
/* create socket */
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
return -1;
}
tls_parm->net_context.fd = sockfd;
flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags|O_NONBLOCK);
memset((char *) &caster, 0x00, sizeof(caster));
memcpy(&caster.sin_addr, he->h_addr, (size_t)he->h_length); //IP address
caster.sin_family = AF_INET; //Address family
port = strtol(ser_port, &b, 10);
caster.sin_port = htons(port);
zlog_info(o, "connecting...");
int res = connect(sockfd, (struct sockaddr *)&caster, sizeof(struct sockaddr_in));
if(0 == res)
{
zlog_info(o, "connect succeed immediately.");
ret = 0;
}
else
{
zlog_info(o, "get the connect result by select.");
if(errno == EINPROGRESS)
{
int times = 0;
while(times++ < 5)
{
fd_set rfds,wfds;
struct timeval tv;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(sockfd, &rfds);
FD_SET(sockfd, &wfds);
tv.tv_sec = 3;
tv.tv_usec = 0;
int selres = select(sockfd + 1, &rfds, &wfds, NULL, &tv);
if(-1 == selres)
{
zlog_error(o, "select error.");
ret = -1;
break;
}
else if(0 == selres)
{
zlog_info(o, "select time out.");
ret = -1;
break;
}
else
{
if(FD_ISSET(sockfd, &rfds) || FD_ISSET(sockfd, &wfds))
{
connect(sockfd, (struct sockaddr *)&caster, sizeof(struct sockaddr_in));
int err = errno;
if(err == EISCONN)
{
zlog_info(o, "connect finished.");
ret = 0;
}
else
{
zlog_error(o, "connect failed,errno=%d", errno);
zlog_error(o, "FD_ISSET(sockfd, &rfds): %d\n FD_ISSET(sockfd, &wfds): %d\n", FD_ISSET(sockfd, &rfds), FD_ISSET(sockfd, &wfds));
ret = errno;
}
}
else
{
ret = -1;
}
}
if(ret != 0)
{
zlog_info(o, "connect again...");
continue;
}
else
{
break;
}
}
}
else
{
zlog_error(o, "connect to host %s:%d failed, errno:%d.\n", he->h_addr, port, errno);
ret = -1;
}
}
return ret;
}
int tcp_send(int sockfd, char *buff, int nbytes)
{
int ret = 0;
int sBytes = 0;
if((sockfd == INVALID_SOCKET) || (buff == NULL) || (nbytes <= 0))
{
zlog_error(o, "Input parameter(s) invalid!");
return -1;
}
do{
ret = send(sockfd, buff+sBytes, (size_t)(nbytes-sBytes), MSG_DONTWAIT);
if(ret <= 0)
{
zlog_error(o, "send error! ret = %d (%d: %s)", ret, errno, strerror(errno));
break;
}
sBytes += ret;
}while(sBytes < nbytes);
return sBytes;
}

2.1.2 server

int tcp_init(short port, int proolflag)
{
int rc;
int fd = -1;
struct sockaddr_in sa;
bzero(&sa, sizeof(sa));
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
return -1;
}
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = htonl(INADDR_ANY);
sa.sin_port = htons(port);
if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0)
{
return -1;
}
rc=1;
if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&rc, sizeof(rc)) < 0)
{
return -1;
}
if (listen(fd, 20) < 0)
{
return -1;
}
return fd;
}
void do_epoll(int listenfd)
{
int epollfd;
struct epoll_event events[EPOLLEVENTS];
int ret;
char buf[MAXSIZE];
memset(buf,0,MAXSIZE);
//创建一个描述符
epollfd = epoll_create(FDSIZE);
//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);
for ( ; ; )
{
//获取已经准备好的描述符事件
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
handle_events(epollfd,events,ret,listenfd,buf);
}
close(epollfd);
}
void handle_accpet(int epollfd,int listenfd)
{
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else
{
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);
//添加一个客户描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}
void do_read(int epollfd,int fd,char *buf)
{
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1)
{
perror("read error:");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else if (nread == 0)
{
fprintf(stderr,"client close.\n");
close(fd);
delete_event(epollfd,fd,EPOLLIN);
}
else
{
printf("read message is : %s",buf);
//修改描述符对应的事件,由读改为写
modify_event(epollfd,fd,EPOLLOUT);
}
}
void do_write(int epollfd,int fd,char *buf)
{
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1)
{
perror("write error:");
close(fd);
delete_event(epollfd,fd,EPOLLOUT);
}
else
modify_event(epollfd,fd,EPOLLIN);
memset(buf,0,MAXSIZE);
}
void add_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
void delete_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
void modify_event(int epollfd,int fd,int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}


阅读(2659) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~