网络编程学习

TCP/IP网络编程
A.服务端客户端网络编程模型
1.网络编程模型:客户端与服务器

服务端一开始就需要监听在一个总所周知的端口上,等待客户端发送请求,一旦有客户端连接建立,服务端就需要消耗一定的计算机资源为他服务,服务端是需要同时为成千上万的客户端服务的,需要高性能网络编程。  客户端相对比较简单,他向服务端的监听端口发起连接的请求,建立连接之后,通过连接通路和服务器进行通讯。 注意无论是服务端还是客户端,他们的运行单位都是进程,而不是机器。

2.服务端编程
(1)调用socket创建套接字
int socket(int domain, int type, int protocol)
domain:即协议域,又称为协议族(family)AF_INET、AF_INET6、AF_LOCAL
type:指定socket类型SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET
protocol:故名思意,就是指定协议IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC
当protocol为0时,会自动选择type类型对应的默认协议
返回值:成功:套接字;失败 < 0
eg:listenfd = socket(AF_INET, SOCK_STREAM, 0)
note:
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口
(2)声明并初始化地址信息结构体变量
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。
bind()函数就是将给这个描述字绑定一个名字。
addrlen:对应的是地址的长度。
addr:一个const struct sockaddr 指针,指向要绑定给sockfd的协议地址。
这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family; /
address family: AF_INET /
in_port_t sin_port; /
port in network byte order /
struct in_addr sin_addr; /
internet address */
};
eg:
// 设置服务器地址结构体
unsigned short port = 8080; // 服务器的端口号
char *server_ip = “10.221.20.10”; // 服务器ip地址
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(port); // 端口
inet_pton(AF_INET, server_ip, &server_addr.sin_addr); // ip
(3)监听
int listen(int sockfd, int backlog);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

	深入理解:
		对于一个调用listen()进行监听的套接字,操作系统会给这个套接字维护两个队列,如上所示。
		未完成连接队列
		当客户端发送TCP三次握手的第一个包(SYN)时,服务器就会在未完成队列中创建一个跟这个syn包对应的一项,
		其实我们也可以把这个连接看成是半连接。这个半连接的状态会从LISTEN变成SYN_RCVD状态,
		同时给客户端返回第二次握手包(syn,ack),这个时候服务器是在等待完成第三次握手。
		已完成连接队列
		当第三次握手完成了,这个连接就变成了ESTABLISHED状态,每个已经完成三次握手的客户端都放在这个队列中作为一项。
		backlog参数之前的含义就是指这两个队列的和。

(4)从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址

	深入理解:
		accept()函数的实质就使用来从已完成连接队列中的队首位置取出来一项(每一项都是一个已经完成三路握手的TCP连接),
		返回给进程,如果已完成连接队列是空的呢?那么咱accept()会一直阻塞(阻塞的前提是我们没有把这个socket设置成非阻塞),直到已完成队列中有一项时才会被唤醒。从编程角度,我们应该尽快用accept()把已完成队列中的数据(TCP连接)取走。
		accept()返回的是个套接字,这个套接字就代表那个已经用三次握手建立起来的那个tcp连接,
		因为accept()是从 已完成队列中取的数据。
		这个套接字需要与监听套接字区别开,监听套接字就是监听端口的那个套接字,只要服务器程序在运行,
		这个套接字就一直在。随后服务器就可以用accept返回的套接字和客户端通信了。

3.客户端编程
(1)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一个参数即为客户端的socket描述字,
第二参数为服务器的socket地址,
第三个参数为socket地址的长度

		深入理解:
		建立socket后默认connect()函数为阻塞连接状态,
		方法一、将socket句柄设置为非阻塞状态。
		方法二、采用信号处理函数设置阻塞超时控制。

4.数据传输,调用网络I/O进行读写操作
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

深入理解send recv:	
1.int send( SOCKET s, const char FAR *buf, int len, int flags );  
	第一个参数指定发送端套接字描述符;
	第二个参数指明一个存放应用,程序要发送数据的缓冲区;
	第三个参数指明实际要发送的数据的字节数;
	第四个参数一般置0。
	如果send函数copy数据成功,就返回实际copy的字节数,
	如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;
	如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR
	同步Socket的send函数的执行流程:
		(1) len > TCP发送缓冲区  return error
		(2)	len < TCP发送缓冲区 
				1.正在发送s的发送缓冲中的数据? 等待发完
				2.没有开始发送s的发送缓冲中的数据/发送缓冲中没有数据
					send就比较s的发送缓冲区的剩余空间和len
					(1)len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,
					(2)如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里。
		
2.int recv( SOCKET s, char FAR *buf, int len, int flags );   
	第一个参数指定接收端套接字描述符;
	第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
	第三个参数指明buf的长度;
	第四个参数一般置0	
	recv函数返回其实际copy的字节数。
	如果recv在copy时出错,那么它返回SOCKET_ERROR;
	如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
		(1)当应用程序调用recv函数时,recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR, 
		(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,
				1.recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到协议把数据接收完毕。
				2.当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中

b.IO多路复用
1.什么是io多路复用

单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力	
本质用更少的资源完成更多的事。

2.目前Linux系统中提供了5种IO处理模型

阻塞IO
		阻塞IO意味着当我们发起一次IO操作后一直等待成功或失败之后才返回,
		在这期间程序不能做其它的事情。阻塞IO操作只能对单个文件描述符进行操作
非阻塞IO
IO多路复用
信号驱动IO
异步IO

3.select函数
常见函数
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:第一个参数是:最大的文件描述符值+1;
readset:可读描述符集合;
writeset:可写描述符集合;
exceptset:异常描述符;
timeout:select 的监听时长,如果这短时间内所监听的 socket 没有事件发生。

1若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
2若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
3timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,

返回值:
负值:select错误,见ERRORS。
正值:某些文件可读写或出错
等待超时,没有可读写或错误的文件
fd_set结构体定义
#define __FD_SETSIZE 1024
typedef __kernel_fd_set fd_set;
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

	注意:
		(1)select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。
		(2)当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。
		(3)select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。
			readfds是需要进行读操作的文件描述符,
			writefds是需要进行写操作的文件描述符,
			exceptfds是需要进行异常事件处理的文件描述符。
			这三个参数可以用NULL来表示对应的事件不需要监听。

void FD_CLR(int fd, fd_set *set);FD_CLR对应将一个文件描述符移出组中
对应于fd文件描述符的位为0
int FD_ISSET(int fd, fd_set *set);检测一个文件描述符是否在组中,查看有哪些文件描述符可以进行IO操作
void FD_SET(int fd, fd_set *set);FD_SET添加一个文件描述符到组中,
对应于fd文件描述符的位为1
void FD_ZERO(fd_set *set);FD_ZERO用来清空文件描述符组。每次调用select前都需要清空一次。

		//每个ulong为32位,可以表示32个bit。
		//fd  >> 5 即 fd / 32,找到对应的ulong下标i;fd & 31 即fd % 32,找到在ulong[i]内部的位置
		#define __FD_SET(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] |= (1<<((fd) & 31)))             //设置对应的bit
		#define __FD_CLR(fd, fdsetp)   (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] &= ~(1<<((fd) & 31)))            //清除对应的bit
		#define __FD_ISSET(fd, fdsetp)   ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] & (1<<((fd) & 31))) != 0)     //判断对应的bit是否为1
		#define __FD_ZERO(fdsetp)   (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp))))

4.epoll函数
int epoll_create(int size);
创建一个epoll实例,文件描述符
该 函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。
size就是你在这个epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符l
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可
返回发生事件数。

epoll_wait运行的原理是
等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。
并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,、
则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。
这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。

typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event
{
uint32_t events; /* Epoll events /
epoll_data_t data; /
User data variable */
};
事件宏
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

libevent 采用水平触发, nginx 采用边沿触发		
水平触发(level-triggered)

socket接收缓冲区不为空 有数据可读 读事件一直触发
socket发送缓冲区不满 可以继续写入数据 写事件一直触发
边沿触发(edge-triggered)

socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
边沿触发仅触发一次,水平触发会一直触发。		

深入理解:

		 epoll使用RB-Tree红黑树去监听并维护所有文件描述符,RB-Tree的根节点
		调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个 红黑树 用于存储以后epoll_ctl传来的socket外,
		还会再建立一个list链表,用于存储准备就绪的事件.
		当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
		所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,
		所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已.
		那么,这个准备就绪list链表是怎么维护的呢?
		当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,
		还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。
		所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
		epoll相比于select并不是在所有情况下都要高效,
		例如在如果有少于1024个文件描述符监听,且大多数socket都是出于活跃繁忙的状态,
		这种情况下,select要比epoll更为高效,因为epoll会有更多次的系统调用,用户态和内核态会有更加频繁的切换。
	
	epoll高效的本质在于:
	减少了用户态和内核态的文件句柄拷贝
	减少了对可读可写文件句柄的遍历
	mmap 加速了内核与用户空间的信息传递,epoll是通过内核与用户mmap同一块内存,避免了无谓的内存拷贝
	IO性能不会随着监听的文件描述的数量增长而下降
	使用红黑树存储fd,以及对应的回调函数,其插入,查找,删除的性能不错,相比于hash,不必预先分配很多的空间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值