GA原有的实现中对并发连接并不支持,表现出的是先启动服务端,服务端通过指定的游戏配置文件来启动特定的游戏,然后等待客户端进行连接,当客户端连接进来时将捕捉到的游戏画面通过H.264压缩后,通过RTP、RTSP等协议将其发送到客户端。当服务端启动一个游戏有多个客户端连接进来时,多个客户端看到的画面是一样的,因为它们都通过相同的端口来接收数据。这种情况并不满足实际使用的需要,为了真正实现并发改造,需要实现一下功能:
1> GA服务端启动后应该一直监听,每当有客户端的请求到来时就建立一个新的链接,根据客户端传递的游戏参数启动对应的游戏。
2> GA accept客户端的连接请求后应该能将该socket保存下来,并在通信时使用,使每个连接过来的客户端能够看到属于自己的画面。
3> 既然能并发连接,那么每个连接过来的客户端就能够单独断开连接而不妨碍其它现有的连接着的客户端。
第一部分的实现:
第一部分的实现需要解决的问题有:1》服务端要能够一直监听客户端的请求。2》服务器端要能够解析客户端传来的参数,从而解析出该客户端需要启动哪个游戏。
服务器端一直监听的实现想必大家都有思路,就是利用socket一直listen就可以了,等到accept之后,启动receiveclient线程,由该线程负责解析参数,以及后期的其它操作,至于socket的使用就不在里详细讲解了,但需要注意的一点就是:Linux和Windows下socket的使用有一些差异,Windows下Socket使用前必须使用WSAStartup()完成对Winsock环境的初始化,相关实现代码如下(注意标红部分的使用):
#ifdef WIN32 WSADATA wd; if(WSAStartup(MAKEWORD(2,2), &wd) != 0) return -1; #endif if((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { fprintf(stderr, "socket creating failed: %s\n", strerror(errno)); return -1; } // do { #ifdef WIN32 BOOL val = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char*) &val, sizeof(val)); #else int val = 1; setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); #endif } while(0); // bzero(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(8554); // if(bind(s, (struct sockaddr*) &sin, sizeof(sin)) < 0) { fprintf(stderr, "socket binding failed: %s\n", strerror(errno)); return -1; } if(listen(s, 256) < 0) { fprintf(stderr, "socket listening failed: %s\n", strerror(errno)); return -1; } // do { int csinlen; pthread_t thread; SOCKET cs; struct sockaddr_in csin; // csinlen = sizeof(csin); bzero(&csin, sizeof(csin)); if((cs = accept(s, (struct sockaddr*) &csin, &csinlen)) < 0) { fprintf(stderr, "socket acceptting error: %s\n", strerror(errno)); return -1; } fprintf(stderr, "client connected from %s:%d\n", inet_ntoa(csin.sin_addr), htons(csin.sin_port)); // tunning sending window do { int sndwnd = 8388608; // 8MB if(setsockopt(cs, SOL_SOCKET, SO_SNDBUF, (const char *)&sndwnd, sizeof(sndwnd)) == 0) { fprintf(stderr, "*** set TCP sending buffer success. cs %d\n", cs); } else { fprintf(stderr, "*** set TCP sending buffer failed.\n"); } } while(0); if(pthread_create(&thread, NULL, receiveclient, &cs) != 0) { // receiveclient线程实现后续解析客户端传来的参数的操作,而且传入cs参数 close(cs); fprintf(stderr, "cannot create receive service thread.\n"); continue; } // pthread_detach() set the thread as detached and when thread exit , OP will recovery the resource occupied by thread pthread_detach(thread); // } while(1);
receiveclient线程创建出来,它的任务就是针对每一个连接,完成后续的操作。原本的实现想法是:直接在accept之后,client与server就进行一次通信,client将游戏参数发送给server,server解析参数、启动游戏,待游戏正常启动后给client发送标记,然后client继续下面的流程。但是过节期间,DIM组领导又将代码分析了一下发现完全没有必要这样实现,在GamingAnywhere的实现中,客户端是利用Live555实现的rtsp通信,逐个执行RTSP的各个阶段,依次是:Option , Describe , Setup , Play , Teardown。在GA实现中它没有使用Option这个阶段,而这是这个阶段的通信可以将客户端启用时命令行的参数传递给服务器端,因此客户端只要多添加:rtspClient->sendOptionsCommand(continueAfterOPTIONS);就可以完成客户端的发送任务,而剩下的就是服务器端的解析,install_hook,启动游戏进程等,这个也很容易实现:
do { int i, fdmax, active; fd_set rfds; struct timeval to; FD_ZERO(&rfds); FD_SET(cs, &rfds); fdmax = cs; to.tv_sec = 0; to.tv_usec = 500000; if((active = select(fdmax+1, &rfds, NULL, NULL, &to)) <= 0) { fprintf(stderr, "socket select failed: %s\n", strerror(errno)); close(cs); cs = 0; return NULL; } if((buflen = recv(cs, (char*) buf, sizeof(buf), 0)) <= 0) { fprintf(stderr, "socek recv failed: %s\n", strerror(errno)); close(cs); cs = 0; return NULL ; } pbuf = buf; get_word(cmd, sizeof(cmd), &pbuf); get_word(url, sizeof(url), &pbuf); get_word(protocol, sizeof(protocol), &pbuf); fprintf(stderr, "get cmd %s, url %s and protocol %s\n", cmd, url, protocol); if(strcmp(protocol, "RTSP/1.0")) { fprintf(stderr, "socket recv not valid protocol information!\n"); close(cs); cs = 0; return NULL; } // check application if(strcmp(cmd, "OPTIONS") != 0) { // RTSP通信的第一阶段:Options fprintf(stderr, "socket recv not valid cmd information!\n"); close(cs); cs = 0; return NULL; }else{ strcpy(value, strstr(url, "8554")+5); char pre_app_name[1024] = "/"; char* new_app_name = strcat(pre_app_name , value); _putenv_s("APP_NAME" , new_app_name); if ((conf_readv(configfile, value, app_exe, app_conf)) < 0){ fprintf(stderr, "read conf failed!\n"); close(cs); cs = 0; //return -1; return NULL ; }else{ fprintf(stderr, "app name: %s, app_conf %s\n", app_exe, app_conf); if (!DuplicateHandle(GetCurrentProcess(), (HANDLE)cs, GetCurrentProcess(), (HANDLE*)&DuplicateSock, 0, TRUE, DUPLICATE_SAME_ACCESS)) { fprintf(stderr,"dup error %d\n",GetLastError()); close(cs); cs = 0; return NULL; } fprintf(stderr , "Before the value of numWindows is %d... .\n" , numWindows); if(numWindows == 0){ fprintf(stderr, "before install_hook!\n"); if ((install_hook(loader_dir, app_conf, app_exe, DuplicateSock)) < 0 ){ // 此函数内实现挂钩子 fprintf(stderr, "install_hook failed!\n"); close(cs); close(DuplicateSock); cs = DuplicateSock = 0; //return -1; return NULL ; } else{ //bFirst = FALSE; numWindows++ ; } }else{ if((add_new_app(loader_dir, app_conf, app_exe, DuplicateSock)) < 0){ fprintf(stderr, "install_hook failed!\n"); close(cs); close(DuplicateSock); cs = DuplicateSock = 0; //return -1; return NULL ; }else{ numWindows++ ; } } ZeroMemory(&procInfo, sizeof(PROCESS_INFORMATION)); ZeroMemory(&startupInfo, sizeof(STARTUPINFO)); if (CreateProcess(app_exe, app_exe, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &startupInfo, &procInfo) == 0) { fprintf(stderr, "CreateProcess failed: 0x%08x\n", GetLastError()); return NULL; } } } } while(0);
还记得前面receiveclient线程创建后,client与server连接的socket(此处以cs表示)就作为参数传进来了,这里会对cs进行一次DuplicateHandle的操作,因为该变量最终是要传递到另一个进程控件去的,因为在执行SetWindowsHookEx(WH_CBT, hook_proc, hInst, 0)后,hook_proc是钩子的回调函数,钩子是挂载在游戏窗口上的,它和当前服务器端程序运行的进程不是同一个,因此cs必须经过转换才能传到新进程空间里去。游戏参数解析完后,读取配置文件,然后CreateProcess()启动游戏进程。
第二部分的实现:
client与server连接的socket(此次以cs表示),进入install_hook()函数里面,该函数是dll里面的函数,它完成的工作就是:1> 将socket变量cs写环境变量,供以后使用;2> SetWindowsHookEx()挂载全局钩子。在后续中会从环境变量中读取出cs的值存入全局变量,标识每个特定的连接。
struct GaEnv { int pid; char ga_app[256]; char ga_config[256]; char ga_root[256]; char ga_module[256]; int cs; }; for(int i=0; i < env_num; i++) { ga_error("[pid %d] ga_env_all %d, pid = %d\n",pid, i, ga_env_all[i].pid); if(ga_env_all[i].pid == pid) { // 保存属于同一线程的变量 strncpy(ga_env_all[i].ga_root, root, strlen(root) + 1); strncpy(ga_env_all[i].ga_config, conf, strlen(conf) +1); ga_env_all[i].cs = (SOCKET)atoi(sock); ga_error("[pid %d] ga_root %s, ga_config %s, socket %d\n", pid, ga_env_all[i].ga_root, ga_env_all[i].ga_config, ga_env_all[i].cs); break; } }
使用cs的地方:
void * ga_server(void *arg) { SOCKET cs; //char sock[8]; int pid = GetCurrentProcessId(); pthread_t thread; // do { usleep(100000); } while(resolution_retrieved == 0); // ga_error("[pid %d ga_server] load modules and run the server\n", pid); // if(vsource_init(encoder_width, encoder_height) < 0) { ga_error("[ga_server] video source init failed.\n"); return NULL; } // prect = NULL; // if(ga_crop_window(&rect, &prect) < 0) { return NULL; } else if(prect == NULL) { ga_error("*** Crop disabled.\n"); } else if(prect != NULL) { ga_error("[pid %d] *** Crop enabled: (%d,%d)-(%d,%d)\n", pid, prect->left, prect->top, prect->right, prect->bottom); } // load server modules if(load_modules() < 0) { return NULL; } if(init_modules() < 0) { return NULL; } if(run_modules() < 0) { return NULL; } for(int i=0; i < env_num; i++) { //ga_error("[pid %d] start rtsp server\n", pid); if(ga_env_all[i].pid == pid) { cs = ga_env_all[i].cs; // 从全局变量中读取出socket //sprintf(sock, "%d", cs); //ga_error("[pid %d] client socket %s, %d\n", pid, sock, cs); break; } } ga_error("[pid %d] client socket %d\n", pid, cs); rtspserver((void *) &cs); 传入rtspserver , 后续完成rtsp的Option以后的操作, ga_deinit(); return NULL; }
此时取出的socket,是针对每个client与server连接建立的socket,接下来的阶段就是将该socket变量存入RTSPContext中,进行RTSP的后续通信,这样每个连接进来的客户端就由于是使用特定的socket因此就会看到属于各自的画面,而且各个连接之间的数据传输互补干扰。
第三部分的实现:
这部分描述的是当客户端需要断开连接时,服务器端也要终止相应的游戏进程,同时当其它client连接进来时也要能正常建立连接,收到服务器端传来的画面。为什么要强调能够接收到服务器端传来的画面呢?这是因为钩子的原因,GA关于启动游戏画面的捕捉是因为在游戏窗口挂载了钩子,而改钩子是全局钩子,也就是说:该全局钩子必须挂载一次,且只能挂载一次。
这里的实现,我们采用一个全局变量numWindows来控制,初始化为0 , 当地一个client进来时,numWindows == 0 , 从而进入install_hook()的函数中,完成全局钩子的挂载,成功执行后numWindows++,等第二个客户端连接进来时执行add_new_app()的操作 , 它与install_hook()的区别仅在于不执行全局钩子的挂载操作。程序中执行完install_hook()或add_new_app()后执行CreateProcee()的操作,启动对应的游戏进程,然后就利用:WaitForSingleObject()来等待游戏进程的结束, 而每个游戏进程的结束就代表者一个连接断开了,从而执行numWindows -- ,这样当numWindows再次变为0时,还是会执行install_hook()全局钩子的挂载操作。
那么如何判断客户端与服务器端的链接断开了呢?GA中的实现是当服务器端无法接受到client端传来的数据时就代表client端断开了与服务器端的连接,从而goto到程序退出的代码段 ,在该代码中我添加了TerminateProcess(GetCurrentProcess() , 0); 从而将游戏进程关闭掉,而游戏进程终止后就自然到了WaitForSingleObject()的地方,从而numWindows--。
以上就是关于GA并发请求的修改,通过这次修改可以实现的功能是:
1〉客户端可以传入游戏参数,启动服务器上安装的指定的游戏。
2> 客户端可以并发连接,服务端一直监听,且每个连接的客户端接收到的游戏画面数据互不干扰。
3> 客户端断开连接后,服务器端关闭相应的游戏,且全部客户端断开连接后还可以再次连接。
通过上面的修改,GA的整个运行更加趋于合理化,也向正式的应用更近了一步。这里要感谢一下DIM的领导穆老师,对并发连接处的修改更多的依赖于穆老师对全局修改的把控。从这里也可以看到,前面文章中提到的一些设计思路在下面的实现中很可能已经发生了改变,就像这次的修改,原有的思路被推翻,而用了GA整合的Live555源码实现中很简单的方式就解决了,这里也提醒我要对代码更加熟悉才行。
下一步的工作:
1> 将用户认证的机制加入GA的实现中。
修改GA的最终目的是提供给VSO平台用户,完成启动、使用应用软件的,因此它必须能够对VSO平台用户提供验证机制,只有在平台注册的用户才能够启动使用服务器上发布的软件。实现思路是:利用C操作Windows AD(活动目录,ldap服务器的一种),VSO平台的用户在完成注册后,账号信息都会写入Windows AD中。可以将用户名和密码在客户端传入服务端,和传入特定游戏参数的实现方式相同,在服务器端将username和password解析出来后,连接Windows AD服务器进行用户验证。关于这部分的准备工作已经完成,即我已经完成了利用C完成Windows AD中用户的增、删、改、查,下周的工作是将该部分的机制整合到GA中,这正实现用户认证。
2> 用户存储的挂载。
首先声明,用户存储挂载这部分是我感觉最为困难的实现方式,因为采用视频捕捉的方式,完全脱离了Windows系统,也就无法依赖它的用户认证以及ACL,面临着如何将属于特定用户的存储目录挂载在服务器下且对其他用户来说不可见。现在关于这块的实现还没有具体的实现思路,希望下周能有个可行的解决方法。