websocket协议与服务器实现

一、WebSocket协议

1、WebSocket和HTTP什么关系

WebSocket和http一样,都是处于OSI模型中的最高层:应用层。

WebSocket借助http协议进行握手,三次握手后,就会变身为TCP通道,从此与http不再相见

WebSocket是一种在单个TCP连接上进行全双工通信的协议。在WebSocket API中,浏览器和服务器只需要完成一次握手(不是指建立TCP连接的那个三次握手,是指在建立TCP连接后传输一次握手数据),两者之间就直接可以创建持久性的连接,并进行双向数据传输。

2、WebSocket和HTTP 的区别

  1. HTTP 是一种请求/响应模型的协议,即客户端发送请求,服务器收到请求后发送响应。而 WebSocket 是一种双向通信协议,客户端和服务器可以通过一个连接同时进行数据的传输和接收,并且数据的传输是实时的,不需要经过轮询或者定时请求。
  2. HTTP 协议是建立在 TCP 协议之上的,每次请求/响应都需要建立一条 TCP 连接,并在处理完毕后主动关闭连接。而 WebSocket 协议也是建立在 TCP 上的,但是建立连接后就可以持久连接,不需要每次请求/响应都需要重新建立连接。
  3. HTTP 请求和响应的数据格式比较严格,要求按照一定的格式进行编写,同时是基于文本的。而 WebSocket 数据格式相对更自由,只要能够被双方解析,就可以进行数据传输,是基于二进制帧的。
  4. HTTP 协议可以通过一些技术手段实现实时消息的推送,如长轮询(long-polling)和短轮询(short-polling),但是实现起来的复杂度较高。而 WebSocket 可以轻松实现实时消息的推送,并且能够实现一些更复杂的应用逻辑,如实时画板、在线游戏等。

总的来说,HTTP 协议适用于客户端向服务器发起请求,服务器做出响应的场景;WebSocket 协议适用于客户端和服务器之间需要实时双向通信的场景,如在线聊天、在线游戏等。

3、WebSocket协议只能浏览器发起么?

不是。目前此协议的受众的也不仅仅是web开发者。

WebSocket只是一种协议,它和http协议一样,使用类似okhttp的组件,可以在任何地方进行调用,甚至可以借助WebSocket实现RPC框架。

4、WebSocket和长轮询有什么区别?

长轮询就是客户端发送一个请求,服务端将一直在这个连接上等待(当然有一个超长的超时时间),直到有数据才返回,它依然是一个一问一答的模式。比如著名的comted。WebSocket在握手成功后,就是全双工的TCP通道,数据可以主动从服务端发送到客户端,处于链接两端的应用没有任何区别。WebSocket创建的连接和Http的长连接是不一样的。由于Http长连接底层依然是Http协议,所以它还是一问一答,只是Hold住了一条命长点的连接而已。长轮询和Http长连接是阻塞的I/O,但WebSocket可以是非阻塞的(具体是多路复用)

5、如何创建一个WebSocket连接?

WebSocket的连接创建是借助Http协议进行的。这样设计主要是考虑兼容性,在浏览器中就可以很方便的发起请求,看起来比较具有迷惑性。

下图是一个典型的由浏览器发起的ws请求,可以看到和http请求长的是非常相似的。

但是,它只是请求阶段长得像而已:

请求的地址: 一般是:ws://***,或者是使用了SSL/TLS加密的安全协议wss:,用来标识是WebSocket请求

1)首先,通过Http头里面的Upgrade域,请求进行协议转换。如果服务端支持的话,就可以切换到WebSocket协议。简单点讲:连接已经在那了,通过握手切换成ws协议,就是切换了连接的一个状态而已。

2)Connection域可以认为是与Upgrade域配对的头信息。像nginx等代理服务器,是要先处理Connection,然后再发起协议转换的。

3)Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。如此操作,可以尽量避免普通 HTTP 请求被误认为 WebSocket 协议。

其他的,像Sec-WebSocket*字样的头信息,表明了客户端支持的子协议以及其他信息。像loT中很流行的mqtt,就可以作为WebSocket的子协议。

6、什么是长连接

HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。

6.1、HTTP协议与TCP/IP协议的关系

HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。 IP协议主要解决网络路由和寻址问题,TCP协议主要解决如何在IP层之上可靠地传递数据包,使得网络上接收端收到发送端所发出的所有包,并且顺序与发送顺序一致。TCP协议是可靠的、面向连接的。

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接

6.2、TCP短连接

模拟一下TCP短连接的情况:client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次请求就完成了。这时候双方任意都可以发起close操作,不过一般都是client先发起close操作。上述可知,短连接一般只会在 client/server间传递一次请求操作。

短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。

6.3、TCP长连接

我们再模拟一下长连接的情况:client向server发起连接,server接受client连接,双方建立连接,client与server完成一次请求后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

TCP的保活功能主要为服务器应用提供。如果客户端已经消失而连接未断开,则会使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,此时服务器将永远等待客户端的数据。保活功能就是试图在服务端器端检测到这种半开放的连接

如果一个给定的连接在两小时内没有任何动作,服务器就向客户发送一个探测报文段,根据客户端主机响应探测4个客户端状态:

客户主机依然正常运行,且服务器可达。此时客户的TCP响应正常,服务器将保活定时器复位。

客户主机已经崩溃,并且关闭或者正在重新启动。上述情况下客户端都不能响应TCP。服务端将无法收到客户端对探测的响应。服务器总共发送10个这样的探测,每个间隔75秒。若服务器没有收到任何一个响应,它就认为客户端已经关闭并终止连接。

客户端崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。

客户机正常运行,但是服务器不可达。这种情况与第二种状态类似。

6.4、长连接和短连接的优点和缺点

由上可以看出,长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户端适合使用长连接。在长连接的应用场景下,client端一般不会主动关闭连接,当client与server之间的连接一直不关闭,随着客户端连接越来越多,server会保持过多连接。这时候server端需要采取一些策略,如关闭一些长时间没有请求发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件允许则可以限制每个客户端的最大长连接数,这样可以完全避免恶意的客户端拖垮整体后端服务。

短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费较多时间和带宽。

长连接和短连接的产生在于client和server采取的关闭策略。不同的应用场景适合采用不同的策略。

由上可以看出,长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说,较适用长连接。不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可 以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。

短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。

长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好

二、websockt代码实现

下面使用了reactor实现了一个简单的websocket服务器,源码及测试过程如下

1、reactor服务器代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/sendfile.h>
#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>

#define BUFFER_LENGTH		1024
#define MAX_EPOLL_EVENTS	1024
#define RESOURCE_LENGTH		1024
#define ACCEPT_KEY_LENGTH	64
#define SERVER_PORT			8888
#define PORT_COUNT			1

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

//websocket固定的GUID
#define GUID	"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

typedef int NCALLBACK(int ,int, void*);

// struct epoll_event {
    // __uint32_t events;  // epoll 监控的事件类型
    // epoll_data_t data;  // 用户数据
// };

// typedef union epoll_data {
    // void *ptr;
    // int fd;
    // __uint32_t u32;
    // __uint64_t u64;
// } epoll_data_t;

//websocket有限状态机
enum {
	WS_HANDSHARK = 0,
	WS_TRANMISSION = 1,
	WS_END = 2,
	WS_COUNT
};

//websocket 数据传输头
struct ws_ophdr 
{
	unsigned char opcode:4,
				rsv3:1,
				rsv2:1,
				rsv1:1,
				fin:1; //client退出标志

	unsigned char pl_len:7,
	 			  mask:1; //加密标志
};

//socketfd子项
struct ntyevent 
{
	int fd;
	int events;
	void *arg; //指向reactor
	int (*callback)(int fd, int events, void *arg);
	
	int status; //是否添加epoll的状态
	
	char rbuffer[BUFFER_LENGTH];
	char wbuffer[BUFFER_LENGTH];
	int rlength;
	int wlength;

	//ws相关
	char sec_accept[ACCEPT_KEY_LENGTH];
	int wsstatus; //0, 1, 2, 3
};

//事件块,每个块中存放 ITEM_LENGTH 个 sock_item
struct eventblock 
{
	struct eventblock *next;//指向下一个 eventblock
	struct ntyevent *events;//数组(由于fd是顺序增加的,可直接将fd与数组的下标一一对应)
};

//epoll的fd,与eventblock事件块管理
struct ntyreactor 
{
	int epfd;
	
	int blkcnt;
	struct eventblock *evblks;
};

void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg);
int nty_event_add(int epfd, int events, struct ntyevent *ev);
int nty_event_del(int epfd, struct ntyevent *ev);
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd);

int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
int accept_cb(int fd, int events, void *arg);

struct timeval tv_begin; //listen时赋初值,用于计算连接所需时长

// 以\r\n为分隔符,读取一行http数据
int http_readline(char* allbuf, int idx, char* linebuf) 
{    
	int len = strlen(allbuf);    

	for (;idx < len; ++idx)    
	{        
		if(allbuf[idx]=='\r' && allbuf[idx+1]=='\n')            
			return idx+2;        
		else            
			*(linebuf++) = allbuf[idx];    
	}    

	return -1;
}

//base64编码
int base64_encode(char *in_str, int in_len, char *out_str) 
{    
	BIO *b64, *bio;    
	BUF_MEM *bptr = NULL;    
	size_t size = 0;    

	if (in_str == NULL || out_str == NULL)        
		return -1;    

	b64 = BIO_new(BIO_f_base64());    
	bio = BIO_new(BIO_s_mem());    
	bio = BIO_push(b64, bio);
	
	BIO_write(bio, in_str, in_len);    
	BIO_flush(bio);    

	BIO_get_mem_ptr(bio, &bptr);    
	memcpy(out_str, bptr->data, bptr->length);    
	out_str[bptr->length-1] = '\0';    
	size = bptr->length;    

	BIO_free_all(bio);    
	return size;
}

// 第一次请求握手解析
int ws_handshark(struct ntyevent *ev) 
{
	int idx = 0;
	char sec_data[128] = {0};
	char sec_accept[128] = {0};

	do {
		char linebuf[1024] = {0};
		idx = http_readline(ev->rbuffer, idx, linebuf);

		//找到请求头中的Sec-WebSocket-Key,取出key
		if (strstr(linebuf, "Sec-WebSocket-Key")) 
		{
			strcat(linebuf, GUID);
			SHA1(linebuf+19, strlen(linebuf+19), sec_data);
			base64_encode(sec_data, strlen(sec_data), sec_accept);		

			printf("idx: %d, line: %ld\n",idx, sizeof("Sec-WebSocket-Key: "));
			memcpy(ev->sec_accept, sec_accept, ACCEPT_KEY_LENGTH);
		}
	} 
	while((ev->rbuffer[idx] != '\r' || ev->rbuffer[idx+1] != '\n') && idx != -1);
}

void ws_umask(char *payload, int length, char *mask_key) 
{
	int i = 0;

	for (i = 0;i < length;i ++) {
		payload[i] ^= mask_key[i%4];
	}
}

int ws_tranmission(struct ntyevent *ev) 
{
	struct ws_ophdr *hdr = (struct ws_ophdr *)ev->rbuffer;

	if (hdr->pl_len < 126) 
	{
		unsigned char *payload = NULL;
		if (hdr->mask) { //启用了加密传输
			payload = ev->rbuffer + 6;

			ws_umask(payload, hdr->pl_len, ev->rbuffer + 2);
		} else {
			payload = ev->rbuffer + 2;
		}

		printf("payload: %s\n", payload);

	} else if (hdr->pl_len == 126) {

	} else if (hdr->pl_len == 127)  {

	} else {
		//assert(0);
	}
}

int ws_request(struct ntyevent *ev) 
{
	if (ev->wsstatus == WS_HANDSHARK) //0
	{
		ws_handshark(ev);
		ev->wsstatus = WS_TRANMISSION;
	} 
	else if (ev->wsstatus == WS_TRANMISSION) //1
	{
		ws_tranmission(ev);
	}
}

int ws_response(struct ntyevent *ev) 
{
	ev->wlength = sprintf(ev->wbuffer, "HTTP/1.1 101 Switching Protocols\r\n"
						"Upgrade: websocket\r\n"
						"Connection: Upgrade\r\n"
						"Sec-WebSocket-Accept: %s\r\n\r\n", ev->sec_accept);

	printf("response: %s\n", ev->wbuffer);
	
	return ev->wlength;
}

//recv时的回调函数
int recv_cb(int fd, int events, void *arg) 
{
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = ntyreactor_idx(reactor, fd);

	if (ev == NULL) 
		return -1;
	
	//接收数据
	int len = recv(fd, ev->rbuffer, BUFFER_LENGTH, 0);
	nty_event_del(reactor->epfd, ev); //先将fd从epoll中删除!!!

	if (len > 0) 
	{
		ev->rlength = len;
		ev->rbuffer[len] = '\0';
		printf("recv [%d]:%s\n", fd, ev->rbuffer);
	
		//解析websocket请求
		ws_request(ev);
		
		//将fd添加进epoll中,设置为EPOLLOUT监听,发送数据
		nty_event_set(ev, fd, send_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLOUT, ev);
	} 
	else if (len == 0) //客户端主动断开连接
	{
		printf("recv_cb --> disconnect\n");
		
		nty_event_del(reactor->epfd, ev); //将fd从epoll中删除
		close(ev->fd);
	} 
	else
	{
		if (errno == EAGAIN && errno == EWOULDBLOCK) {
			
		} else if (errno == ECONNRESET){
			nty_event_del(reactor->epfd, ev);
			close(ev->fd);
		}
		printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
	}

	return len;
}

//send时的回调函数
int send_cb(int fd, int events, void *arg) 
{
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	struct ntyevent *ev = ntyreactor_idx(reactor, fd);

	if (ev == NULL) 
		return -1;

	ws_response(ev);
	
	//没有数据要发生
	if(ev->wlength == 0)
	{
		//将fd添加进epoll中,设置为EPOLLIN监听
		nty_event_del(reactor->epfd, ev);
		nty_event_set(ev, fd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, ev);
		
		return 0;
	}

	int len = send(fd, ev->wbuffer, ev->wlength, 0);
	if (len > 0) 
	{
		//将fd添加进epoll中,设置为EPOLLIN监听
		nty_event_del(reactor->epfd, ev);
		nty_event_set(ev, fd, recv_cb, reactor);
		nty_event_add(reactor->epfd, EPOLLIN, ev);
	} 
	else 
	{
		nty_event_del(reactor->epfd, ev);
		close(ev->fd);

		printf("send[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
	}

	return len;
}


//新client连接时的accept处理回调函数
int accept_cb(int fd, int events, void *arg) 
{
	static int curfds = 0;
	struct ntyreactor *reactor = (struct ntyreactor*)arg;
	if (reactor == NULL) 
		return -1;

	struct sockaddr_in client_addr;
	socklen_t len = sizeof(client_addr);
	int clientfd;

	//accept连接新的client
	if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) 
	{
		if (errno != EAGAIN && errno != EINTR) {
			
		}
		printf("accept: %s\n", strerror(errno));
		return -1;
	}

	//将clientfd设置成非阻塞
	int flag = 0;
	if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
		printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
		return -1;
	}

	//根据sockfd从reactor中 找到对应的ntyevent
	struct ntyevent *event = ntyreactor_idx(reactor, clientfd);
	if (event == NULL) 
		return -1;
		
	//将clientfd对应的ntyevent添加到epoll中
	nty_event_set(event, clientfd, recv_cb, reactor);
	nty_event_add(reactor->epfd, EPOLLIN, event);

	//计算连接所花的时间
	if (curfds++ % 1000 == 999) 
	{
		struct timeval tv_cur;
		memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
		
		gettimeofday(&tv_begin, NULL);

		int time_used = TIME_SUB_MS(tv_begin, tv_cur);
		printf("connections: %d, sockfd:%d, time_used:%d\n", curfds, clientfd, time_used);
	}

	printf("new connect [%s:%d], pos[%d]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd);

	return 0;
}

//设置ntyevent,对各参数赋值
//arg:指向reactor
void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg) 
{
	ev->fd = fd;
	ev->callback = callback;
	ev->events = 0;
	ev->arg = arg;
	//ev->last_active = time(NULL);

	return ;
}

//将fd添加进epoll中
int nty_event_add(int epfd, int events, struct ntyevent *ev) 
{
	struct epoll_event ep_ev = {0, {0}};
	
	ev->events = events;
	
	ep_ev.data.ptr = ev; //指向fd对应的ntyevent
	ep_ev.events = events;

	int op;
	if (ev->status == 1) {
		op = EPOLL_CTL_MOD;
	} else {
		op = EPOLL_CTL_ADD;
		ev->status = 1; //fd第一次添加完后,之后都是 EPOLL_CTL_MOD 操作
	}

	if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {
		printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		return -1;
	}

	return 0;
}

//将fd从epoll中删除
int nty_event_del(int epfd, struct ntyevent *ev) 
{
	struct epoll_event ep_ev = {0, {0}};

	//如果没有添加过,直接返回
	if (ev->status != 1) {
		return -1;
	}

	ep_ev.data.ptr = ev; //指向fd对应的ntyevent
	ev->status = 0;
	epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);

	return 0;
}

//添加新块,扩容
int ntyreactor_alloc(struct ntyreactor *reactor) 
{
	if (reactor == NULL) return -1;
	if (reactor->evblks == NULL) return -1;
	
	struct eventblock *blk = reactor->evblks;
	
	//定位到最后一个next为NULL的节点
	while (blk->next != NULL) {
		blk = blk->next;
	}

	//申请事件块中的数组空间
	struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
	if (evs == NULL) {
		printf("ntyreactor_alloc ntyevent failed\n");
		return -2;
	}
	memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
	//申请事件块节点空间
	struct eventblock *block = malloc(sizeof(struct eventblock));
	if (block == NULL) {
		printf("ntyreactor_alloc eventblock failed\n");
		return -3;
	}
	
	//事件块赋值
	block->events = evs;
	block->next = NULL;

	//添加到reactor
	blk->next = block;
	reactor->blkcnt ++;

	return 0;
}

//根据sockfd从reactor中 找到对应的ntyevent
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd)
{
	int i = 0;
		
	if (reactor == NULL) return NULL;
	if (reactor->evblks == NULL) return NULL;

	//计算sockfd对应块的索引(从0开始),如果超过了现有管理的块数后,会添加新的块来扩容
	//sockfd 0-1023在1块、sockfd 1024-2047在2块、...
	int blkidx = sockfd / MAX_EPOLL_EVENTS;
	while (blkidx >= reactor->blkcnt) {
		ntyreactor_alloc(reactor);
	}

	//定位到sockfd所在的块
	struct eventblock *blk = reactor->evblks;
	while (i++ != blkidx && blk != NULL) {
		blk = blk->next;
	}

	//返回ntyevent,sockfd % ITEM_LENGTH 就是 sockfd在数组中的索引
	return &blk->events[sockfd % MAX_EPOLL_EVENTS];
}

//初始化reactor:创建epoll、创建1个事件管理块
int ntyreactor_init(struct ntyreactor *reactor) 
{
	if (reactor == NULL) return -1;
	memset(reactor, 0, sizeof(struct ntyreactor));

	//1、创建epoll
	reactor->epfd = epoll_create(1);
	if (reactor->epfd <= 0) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		return -2;
	}

	//2、创建1个事件管理块
	struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
	if (evs == NULL) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		close(reactor->epfd);
		return -3;
	}
	memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));

	struct eventblock *block = malloc(sizeof(struct eventblock));
	if (block == NULL) {
		free(evs);
		close(reactor->epfd);
		return -3;
	}
	block->events = evs;
	block->next = NULL;

	reactor->evblks = block;
	reactor->blkcnt = 1;

	return 0;
}

//销毁rector
int ntyreactor_destory(struct ntyreactor *reactor) 
{
	close(reactor->epfd);

	struct eventblock *blk = reactor->evblks;
	struct eventblock *blk_next;
	while (blk != NULL) {
		blk_next = blk->next;

		free(blk->events);
		free(blk);
		
		blk = blk_next;
	}

	return 0;
}

//往reactor中添加listen事件
int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor) 
{
	if (reactor == NULL) return -1;
	if (reactor->evblks == NULL) return -1;

	struct ntyevent *event = ntyreactor_idx(reactor, sockfd);
	if (event == NULL) return -1;

	nty_event_set(event, sockfd, acceptor, reactor);
	nty_event_add(reactor->epfd, EPOLLIN, event);

	return 0;
}

//监听epoll
int ntyreactor_run(struct ntyreactor *reactor) 
{
	if (reactor == NULL) return -1;
	if (reactor->epfd < 0) return -1;
	if (reactor->evblks == NULL) return -1;
	
	struct epoll_event events[MAX_EPOLL_EVENTS+1];
	int checkpos = 0, i;

	while (1)
	{
		int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
		if (nready < 0) {
			printf("epoll_wait error, exit\n");
			continue;
		}

		for (i = 0;i < nready;i ++) 
		{
			//data.ptr指向fd对应的ntyevent
			struct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;

			//ntyevent->arg指向reactor
			if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
			if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
		}
	}
}

//初始化server,完成 socket--->bind--->listen
//ip:INADDR_ANY
//port:自定义
int init_sock(short port) 
{
	int fd = socket(AF_INET, SOCK_STREAM, 0);
	fcntl(fd, F_SETFL, O_NONBLOCK);

	struct sockaddr_in server_addr;
	memset(&server_addr, 0, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(port);

	bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

	if (listen(fd, 20) < 0) {
		printf("listen failed : %s\n", strerror(errno));
		return -1;
	}

	printf("listen server port : %d\n", port);
	gettimeofday(&tv_begin, NULL);

	return fd;
}


//gcc -o ../bin/07_reactor_singlecb_websocket 07_reactor_singlecb_websocket.c -lssl -lcrypto
int main(int argc, char *argv[]) 
{
	int i = 0;
	int sockfds[PORT_COUNT] = {0};
	struct ntyreactor *reactor = NULL;
	
	//创建并初始化reactor
	reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
	ntyreactor_init(reactor);

	//初始化server,开启监听
	unsigned short port = SERVER_PORT;
	if (argc == 2) {
		port = atoi(argv[1]);
	}
	for (i = 0;i < PORT_COUNT;i ++) {
		sockfds[i] = init_sock(port+i);
		ntyreactor_addlistener(reactor, sockfds[i], accept_cb);
	}
	
	//监听io事件
	ntyreactor_run(reactor);

	//销毁rector
	ntyreactor_destory(reactor);
	
	for (i = 0;i < PORT_COUNT;i ++) {
		close(sockfds[i]);
	}
	free(reactor);
	
	return 0;
}

2、编译

编译时需要链接 ssl、crypto 两个动态库

gcc -o reactor_singlecb_websocket reactor_singlecb_websocket.c -lssl -lcrypto

3、测试

(1)启动程序后,打开websocket.html 测试网页,html如下

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>本地websocket测试</title>
        <meta name="robots" content="all" />
        <meta name="keywords" content="本地,websocket,测试工具" />
        <meta name="description" content="本地,websocket,测试工具" />
        <style>
            .btn-group{
                display: inline-block;
            }
        </style>
    </head>
    <body>
        <input type='text' value='ws://localhost:3000/api/ws' class="form-control" style='width:390px;display:inline'
         id='wsaddr' />
        <div class="btn-group" >
            <button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
            <button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
            <button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
        </div>
        <div class="row">
            <div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div>
            <input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button" onclick="doSend();">发送</button>
            </span>
            </div>
        </div>
    </body>     
        
        <script crossorigin="anonymous" integrity="sha384-LVoNJ6yst/aLxKvxwp6s2GAabqPczfWh6xzm38S/YtjUyZ+3aTKOnD/OJVGYLZDl" src="https://lib.baomitu.com/jquery/3.5.0/jquery.min.js"></script>
        <script language="javascript" type="text/javascript">
            function formatDate(now) {
                var year = now.getFullYear();
                var month = now.getMonth() + 1;
                var date = now.getDate();
                var hour = now.getHours();
                var minute = now.getMinutes();
                var second = now.getSeconds();
                return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
                    " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
                        second = second < 10 ? ("0" + second) : second);
            }
            var output;
            var websocket;
 
            function init() {
                output = document.getElementById("output");
                testWebSocket();
            }
 
            function addsocket() {
                var wsaddr = $("#wsaddr").val();
                if (wsaddr == '') {
                    alert("请填写websocket的地址");
                    return false;
                }
                StartWebSocket(wsaddr);
            }
 
            function closesocket() {
                websocket.close();
            }
 
            function StartWebSocket(wsUri) {
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
                websocket.onclose = function(evt) {
                    onClose(evt)
                };
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
                websocket.onerror = function(evt) {
                    onError(evt)
                };
            }
 
            function onOpen(evt) {
                writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
            }
 
            function onClose(evt) {
                writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
                websocket.close();
            }
 
            function onMessage(evt) {
                writeToScreen('<span style="color:blue">服务端回应&nbsp;' + formatDate(new Date()) + '</span><br/><span class="bubble">' +
                    evt.data + '</span>');
            }
 
            function onError(evt) {
                writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
            }
 
            function doSend() {
                var message = $("#message").val();
                if (message == '') {
                    alert("请先填写发送信息");
                    $("#message").focus();
                    return false;
                }
                if (typeof websocket === "undefined") {
                    alert("websocket还没有连接,或者连接失败,请检测");
                    return false;
                }
                if (websocket.readyState == 3) {
                    alert("websocket已经关闭,请重新连接");
                    return false;
                }
                console.log(websocket);
                $("#message").val('');
                writeToScreen('<span style="color:green">你发送的信息&nbsp;' + formatDate(new Date()) + '</span><br/>' + message);
                websocket.send(message);
            }
 
            function writeToScreen(message) {
                var div = "<div class='newmessage'>" + message + "</div>";
                var d = $("#output");
                var d = d[0];
                var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
                $("#output").append(div);
                if (doScroll) {
                    d.scrollTop = d.scrollHeight - d.clientHeight;
                }
            }
 
 
            function en(event) {
                var evt = evt ? evt : (window.event ? window.event : null);
                if (evt.keyCode == 13) {
                    doSend()
                }
            }
        </script>
 
</html>

输入服务器的地址址,我这里是 ws://192.168.154.149:8888 

点击连接后,程序打印如下信息:

(2)发送数据

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值