实现一个WEBIM

除了用C写过hello world,数据结构,第一次写这么多行。高手请忽略我。

IM实现的方式现在有很多,我挑了一种来实践了一下。前端用jsonp发异步还能跨域的长轮询请求,后端用epoll写了一个支持长连接的chat server

前端方面

接收IM消息:发起一个http请求,这个请求在服务器端一直不返回,是个长连接。当服务器有信息反馈的时候,再发送一个长连接请求。

这个也叫长轮询,是服务器推实现方式的一种。

发送IM消息:发起一个http请求,将发送文本发给服务器,服务器根据发送对象,给出哪个长连接可以返回,这里就是短连接了。

后端方面

有多少人在线就得有多少个长连接一直在后端运行,所以不考虑一个用户一个线程的后端这种处理方式,可以考虑单线程,IO多路复用的非阻塞模型(epoll)。

前端客户端

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>客户端</title>
</head>
<body>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js"></script>
<script type="text/javascript">

function recv(){
	$.getJSON('http://142.54.174.134:8080/recv.html?cmd=login&callback=?',function(data){
		$("ul").append("<li>"+data+"</li>"); 
		// $("#resp").html(data);
		recv(); //这里就是在长轮询了,当长连接有数据返回,别且更新完html上的dom把它显示出来以后,再发起一个长连接,等待下次接受聊天
	});
}

recv();

function sendCmd(msg){
	$.getJSON('http://142.54.174.134:8080/send.html?cmd=notify:' + msg + '&callback=?',function(data){
		//$("#resp").html(data);
	});
}
function go(){
	sendCmd($("#exec_string").val());
}
</script>
<form>
	<div>响应数据</div>
	<div id="resp" style="height:300px">
		<ul>   
	      
	    </ul>
	</div>
	<textarea id="exec_string" style="height:100px"></textarea>
	<input type="button" onclick="go()" value="excute"></input>
</form>


</body>
</html>

后端服务器

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

#include "epoll.h"
#include "sock.h"

static volatile sig_atomic_t shutdown_flag = 0;
static int efd;

void signal_handler(int sig){
    printf("server shutdown\r\n");
    shutdown_flag = 1;
}

void notify_all(struct User* user, int self){
    int i=0;
    for(i=0;i<EPOLL_SIZE;i++){
        if(i!=self && user[i].in_use){
            user[i].resp = user[self].resp;
            socket_send(i, user);
            epoll_del(efd, i);
            close(i);
            free(user[i].callback);
            free(user[i].cmd);
            bzero(&user[i],sizeof(struct User));    
        }
    }
    //responce self
    socket_send(self, user);
    epoll_del(efd, self);
    close(self);
    free(user[self].callback);
    free(user[self].cmd);
    free(user[self].resp);
    bzero(&user[self],sizeof(struct User));    
}

int main(){
    signal(SIGTERM, signal_handler);
    signal(SIGINT, signal_handler);
    int server_sockfd, client_sockfd;
    int ret,addr_len = sizeof(struct sockaddr);
    struct sockaddr_in client_addr;
    struct epoll_event events[EPOLL_SIZE];
    struct User user[EPOLL_SIZE];
    
    bzero(&user,sizeof(user));
    //create
    server_sockfd = create();
 
    //bind
    bind2sock(server_sockfd, PORT); 
    
    //listen
    listening(server_sockfd); 
    
    efd = epoll_init(EPOLL_SIZE);

    epoll_prepare_fd(server_sockfd);
    epoll_add(efd, server_sockfd);
    int nfds,i=0;
    //accept loop
    while(!shutdown_flag){
        nfds = epoll_wait(efd, events, EPOLL_SIZE, -1);
        for(i=0;i<nfds;i++){
            // printf("triggered %d fds\r\n",nfds);
            if(events[i].data.fd==server_sockfd){
                while(1){
                    client_sockfd = accept(server_sockfd, (struct sockaddr*)(&client_addr), &addr_len);
                    if(client_sockfd<0){
                        break;
                    }
                    epoll_prepare_fd(client_sockfd); 
                    epoll_add(efd, client_sockfd); 
                    user[client_sockfd].sockfd = client_sockfd;
                    user[client_sockfd].port =  client_addr.sin_port;
                    user[client_sockfd].ip =  (char*)inet_ntoa(client_addr.sin_addr);
                    user[client_sockfd].callback = NULL;
                    user[client_sockfd].cmd = NULL;
                    user[client_sockfd].resp = NULL;
                    user[client_sockfd].in_use = 1;
                }
            }else if(events[i].events==EPOLLIN){
                // printf("epoll in \r\n");
                client_sockfd = events[i].data.fd;
                if(socket_recv(client_sockfd, user) < 0){
                    printf("close when epoll in\r\n");
                    epoll_del(efd, client_sockfd);
                    close(client_sockfd);
                    bzero(&user[client_sockfd],sizeof(struct User));       
                }else{
                    epoll_set(efd, client_sockfd, EPOLLOUT);  
                }
            }else if(events[i].events==EPOLLOUT){
                // printf("epoll out \r\n");
                client_sockfd = events[i].data.fd;
                if(user[client_sockfd].cmd && strstr(user[client_sockfd].cmd,"notify")){
                    printf("brocast msg\r\n");
                    user[client_sockfd].resp = strdup(strstr(user[client_sockfd].cmd,":"));
                    notify_all(user, client_sockfd);          //当发送连接来的时候,就把其他都在等待的长连接都返回。并把发送的聊天数据作为返回。
                }else{
                    epoll_set(efd, client_sockfd, EPOLLIN);    //长连接的保持
                }
            }else{
                printf("other case \r\n");
                close(events[i].data.fd);
                epoll_del(efd, events[i].data.fd);
                free(user[events[i].data.fd].callback);
                free(user[events[i].data.fd].cmd);
                free(user[client_sockfd].resp);
                bzero(&user[events[i].data.fd],sizeof(struct User));
                close(events[i].data.fd);
            }
        }
    }
    close(efd);
    close(server_sockfd);
    return 0;
}

打开页面的时候就发了个http://142.54.174.134:8080/recv.html?cmd=login&callback=?长连接请求,等待接收服务器数据。

a页面发送了一个i am a,b页面发送了一个i am b

————————————————————————————————————————————————

一个完整的IM server还应该考虑

1.超时问题

比如浏览器页面关闭,或者网络出现问题等。导致长连接一直没关闭从而占用服务器epoll event,一种办法是服务器定期发送心跳消息。

2.后端负载

单机情况,随着用户的增张,EPOLL_SIZE就需要更大。这样肯定不行,估计就要考虑一些切分,应该和网游的分服差不多。

3.处理一些验证,离线留言等等。

其实经常看有些成熟的web im 都是在chat server和客户端之间加一层php这种东西处理一些业务逻辑,用php来写业务逻辑肯定比c好些,而且也好维护。尽量还是让c这一层可以轻一些,以后好维护。

参考

Comet:基于 HTTP 长连接的“服务器推”技术

JSONP跨域原理和jQuery.getJSON用法

epoll 使用详解

nextIM

原文地址:https://www.cnblogs.com/23lalala/p/2911199.html