除了用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这一层可以轻一些,以后好维护。
参考