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