1.聊天窗口的设计
本聊天室分为服务器端和客户端两部分,采用GTK+2.0,即可用简短的代码来编写窗口并向窗口中插入各个控件,通过灵活地使用信号/回调函数机制,实现用户登录、通信连接、信息发送、信息接收等功能。首先运行服务器端(如图1)等待客户端连接,再运行客户端,单击“登录”按钮输入用户名即可与服务器端连接,然后双方或多方就可以进行通信了(如图2),注意服务器只进行聊天信息的转发。
2.聊天过程的实现
2.1系统流程图
聊天过程采用TCP/IP协议下的Client/Server网络通信模式实现,通过套接字(Socket)接口可方便的实现TCP、UDP传输协议完成数据的网络传输,对可靠性要求高的数据通讯系统往往使用TCP协议传输数据,故该聊天室采用TCP协议传输数据。在双方进行通信前,要先运行服务器端程序,等待客户端的连接。
聊天室程序设计的思路是由一个服务器端程序和一个聊天者端程序组成。服务器端程序主要负责记录所有进入本聊天室的聊天者的IP地址,并且接收所有聊天者的信息,将每个聊天者发来的信息转发给所有聊天者。聊天者程序可以发送聊天信息给服务器,同时可以接收服务器发送回来的信息,并显示到聊天记录界面上。根据TCP传输控制协议,Socket编程的基本函数socket()、bind()、listen()、accept()、send()、recv(),结合该聊天室的具体情况,给出系统的工作流程图如图3所示。
2.2具体步骤
上一节主要介绍了系统的工作流程图,这一节将对每个函数进行说明,并给出具体的步骤。
(1)建立一个socket通信
调用函数socket(int family, int type, int protocol)来建立一个套接字,即向系统注册,通知系统建立一个通信端口。参数family表示所采用的协议族,此处取值为AF_INET,即IPv4协议;type为套接字接口的类型描述,取值为SOCK_STREAM,表示字节流套接字,可以理解为TCP套接字接口类型;protocol表示socket所使用传输协议的编号,通常取值为0。若成功则返回一个socket描述符。
(2)对socket定位
调用函数bind(int sockfd, struct sockaddr *my_addr, int addrlen)将新建的套接字与本地IP地址联系起来,若绑定其他IP地址则不能成功。sockfd即为调用socket函数后所返回的socket描述符;my_addr为包含本机IP地址和端口号的指针;addrlen 为地址长度,即sockaddr的结构长度。
(3)等待客户端的连接
调用函数listen(int sockfd, int backlog)使socket处于监听模式,会创建一个等待队列,在其中存放未处理的客户端连接请求。参数backlog表示请求队列中允许的最大请求数,大多数系统默认值为5。listen0并未开始接收连线,只是设置socket为监听模式,调用accept()成功后才是真正接收客户端的连线。所以listen()应该在socket(),bind()之后,在acept0之前调用。
(4)接收客户端socket连线
调用函数accept(int sockfd, struct sockaddr *addr, int *addrlen)来接收客户端的连线请求。它通常从由listen()所创建的等待队列中取出第一个未处理的连接请求。sockfd是被监听的socket的描述符:连线成功后,addr所指的结构会被填入客户端主机的地址数据;addrlen是sockaddr的结构长度。若连接成功则返回新的socket处理代码。
相对服务器端复杂的过程而言,客户端的工作比较简单。
运行客户端程序后,在登录框输入用户名,然后程序会向提前绑定了IP地址的服务器端发送连接请求。首先调用函数socket(),像服务器端那样建立一个套接字,然后调用函数connect(int sockfd, struct sockaddr *serv_ addr, int addrlen)将客户端连接至服务器端。sockfd即为新建的客户端的socket描述符;serv_addr所指向的结构为服务器端的地址;参数addrlen为sockaddr的结构长度。
当两个及两个以上的客户端与服务器端连接成功后,双方或多方即可进行通信,服务器端只进行转发消息。send()和 recv()这两个函数用来在面向连接的socket上进行数据传输,其调用方式如下:
1)函数send(int sockfd, const void *msg, int len, int fags)用来发送数据。sockfd 是建立好连接的socket描述符;msg指针指向要发送的数据;len是以字节为单位的数据的长度,flags设置为0。
2)函数recv(int sockfd, void *buf, int len, unsigned int flags)用来接收数据。sockfd是建立好连接的socket描述符;buf指向存放接收数据的缓冲区;len是缓冲的长度; flags设置为0。
2.3多线程技术
进入聊天程序后,程序要不断检测是否有新的信息发送过来,如果只是简单的采用无限循环这个操作,程序会进入死机状态,此时就无法进行发送信息等其他的操作了,而使用多线程技术[6],就可将接收信息的操作置于一个新的线程,从而避免无法发送信息的情况。所谓多线程,就是将一个进程分成多个执行线程,各个线程可以独立运行。多线程程序采用一种多任务、并发的工作方式,主要优点有:提高应用程序的响应;更有效的使用多处理器;改善程序结构;占用较少的系统资源。
本系统客户端采用主线程发送信息,并且开辟一个新的线程用于接收信息。这样当程序运行时,就不至于产生阻塞而导致无法发送信息的情况发生。而服务器端由于只进行信息的转发,所以收发信息可以都放到在一个线程内,提高信息转发效率。每个线程共享CPU,操作系统为每个线程分配不同的CPU时间片,由于每个时间片的时间很短,虽然实际上同一时刻只有一个线程在运行,但是看上去好像多个线程是并发运行。
若使用线程,在初始化GTK+库函数之前必须运行g_thread_init(NULL)和gdk_threads_init()来初始化线程应用。创建线程调用函数g_thread_create(),如g_thread_create((GThreadFunc)get_message,NULL,FALSE, NULL)中,g _thread_create()函数用来生成接收消息的线程,get_ message()即为线程的具体事件回调函数。当线程例程(即线程执行的代码)开始时,通过gdk_threads_enter()来获得一个唯一的全局锁,当线程例程返回时,通过gdk_ threads_leave()释放该全局锁。线程创建成功后,新创建的线程开始运行回调函数且不影响原来的线程继续运行。
3.代码实现
服务器端代码(server.c):
#include <glib.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>#define OURPORT 4321
#define MAX_USERS 8struct _client
{gint sd;gboolean in_use;gchar name[64];gchar buf[1024];
};
typedef struct _client client;
client user[MAX_USERS];void do_service(gpointer id)
{gint j;gchar tobuf[1024] = {0};gint num = -1;while(num = recv(user[GPOINTER_TO_INT(id)].sd, user[GPOINTER_TO_INT(id)].buf, 1024,0)) {if (num == -1 || num == 0) break;sprintf(tobuf, "%s:%s\n", user[GPOINTER_TO_INT(id)].name, user[GPOINTER_TO_INT(id)].buf);for(j = 0; j < MAX_USERS; j++) {if (user[j].in_use) {send(user[j].sd, tobuf, 1024,0);printf("%s", tobuf);}}}user[GPOINTER_TO_INT(id)].in_use = FALSE;close(user[GPOINTER_TO_INT(id)].sd);
}
int main(int argc, char *argv[])
{gint sd, newsd;struct sockaddr_in *sin;gint slen;gint count = 0;gint flags;gchar buf[1024];gchar tobuf[1024];gint length, i, j;if (!g_thread_supported()) {g_thread_init(NULL);}else {g_print("thread not supported\n");}sd = socket(AF_INET, SOCK_STREAM, 0);if (sd == -1) {g_print("create socket error!\n");return -1;}sin = g_new(struct sockaddr_in, 1);sin->sin_family = AF_INET;sin->sin_addr.s_addr=inet_addr("192.168.0.23");sin->sin_port = htons(OURPORT);slen = sizeof(struct sockaddr_in);if (bind(sd, (struct sockaddr*)sin, slen) < 0) {g_print("bind error!\n");return -1;}if (listen(sd, 8) < 0) {g_print("listen error!\n");return -1;}for (i = 0; i < MAX_USERS; i++) {user[i].in_use = FALSE;}flags = fcntl(sd, F_GETFL);fcntl(sd, F_SETFL, flags &~O_NDELAY);for(;;) {newsd = accept(sd, (struct sockaddr*)sin, (socklen_t *)&slen);if (newsd == -1) {g_print("accept error!\n");break;}else {if (count >= MAX_USERS) {sprintf(buf, "用户数量过多服务器不能通讯。\n");write(newsd, buf, 1024);close(newsd);}else {flags = fcntl(user[i].sd, F_GETFL);fcntl(user[i].sd, F_SETFL, O_NONBLOCK);user[count].sd = newsd;user[count].in_use = TRUE;read(newsd, user[count].name, 64);g_thread_create((GThreadFunc)do_service, (gpointer)count, TRUE, NULL);count++;}}}close(sd);g_free(sin);
}
客户端代码(client.c):
#include <gtk/gtk.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>#define OURPORT 4321
gint sd;
struct sockaddr_in s_in;
gchar username[64];
gchar buf[1024];
gchar get_buf[1048];
gboolean isconnected = FALSE;static GtkWidget *text;
static GtkTextBuffer *buffer;
static GtkWidget *message_entry;
static GtkWidget *name_entry;
static GtkWidget *login_button;void get_message()
{GtkTextIter iter;gchar get_buf[1024];gchar buf[1024];gint num = -1;while(num = recv(sd, buf, 1024,0)) {if (num == -1 || num == 0) break;sprintf(get_buf, "%s", buf);gdk_threads_enter();gtk_text_buffer_get_end_iter(buffer, &iter);gtk_text_buffer_insert(buffer, &iter, get_buf, -1);gdk_threads_leave();}
}gboolean do_connect_run()
{struct hostent *host;GtkTextIter iter;gint slen;sd = socket(AF_INET, SOCK_STREAM, 0);if (sd < 0) {gtk_text_buffer_get_end_iter(buffer, &iter);gtk_text_buffer_insert(buffer, &iter, "打开套接字时出错!\n", -1);return FALSE;}s_in.sin_family = AF_INET;host=gethostbyname("192.168.0.23");s_in.sin_addr=*((struct in_addr *)host->h_addr);s_in.sin_port = htons(OURPORT);slen = sizeof(s_in);if (connect(sd, (struct sockaddr*)&s_in, slen) < 0) {gtk_text_buffer_get_end_iter(buffer, &iter);gtk_text_buffer_insert(buffer, &iter, "连接服务器时出错!\n", -1);return FALSE;}else {gtk_text_buffer_get_end_iter(buffer, &iter);gtk_text_buffer_insert(buffer, &iter, username, -1);gtk_text_buffer_get_end_iter(buffer, &iter);gtk_text_buffer_insert(buffer, &iter, "成功与服务器连接...\n", -1);write(sd, username, 64);isconnected = TRUE;return TRUE;}
}
void on_destroy(GtkWidget *widget, GdkEvent *event, gpointer data)
{sprintf(username, "guest");if(do_connect_run() == TRUE) {gtk_widget_set_sensitive(login_button, FALSE);g_thread_create((GThreadFunc)get_message, NULL, FALSE, NULL);}gtk_widget_destroy(widget);
}
void on_button_clicked(GtkButton *button, gpointer data)
{const gchar *name;name = gtk_entry_get_text(GTK_ENTRY(name_entry));sprintf(username, "%s", name);if (do_connect_run()) {gtk_widget_set_sensitive(login_button, FALSE);g_thread_create((GThreadFunc)get_message, NULL, FALSE, NULL);}gtk_widget_destroy(GTK_WIDGET(data));
}
void create_win()
{GtkWidget *win, *vbox;GtkWidget *button;win = gtk_window_new(GTK_WINDOW_TOPLEVEL);g_signal_connect(G_OBJECT(win), "delete_event", G_CALLBACK(on_destroy), NULL);gtk_window_set_title(GTK_WINDOW(win), "输入用户名");gtk_window_set_position(GTK_WINDOW(win), GTK_WIN_POS_CENTER);gtk_container_set_border_width(GTK_CONTAINER(win), 10);gtk_window_set_modal(GTK_WINDOW(win), TRUE);vbox = gtk_vbox_new(FALSE, 0);gtk_container_add(GTK_CONTAINER(win), vbox);name_entry = gtk_entry_new();gtk_box_pack_start(GTK_BOX(vbox), name_entry, TRUE, TRUE, 5);button = gtk_button_new_from_stock(GTK_STOCK_OK);g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(on_button_clicked), win);gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 5);gtk_widget_show_all(win);
}
void on_send(GtkButton *button, gpointer data)
{const gchar *message;if (isconnected == FALSE) return;message = gtk_entry_get_text(GTK_ENTRY(message_entry));if (g_strcmp0(message, "") == 0) return;sprintf(buf, "%s", message);send(sd, buf, 1024,0);gtk_entry_set_text(GTK_ENTRY(message_entry), "");
}
void on_login(GtkWidget *button, gpointer data)
{create_win();
}
void on_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
{close(sd);gtk_main_quit();
}
int main(int argc, char *argv[])
{GtkWidget *window;GtkWidget *vbox, *hbox, *button, *label, *view;if (!g_thread_supported()) {g_thread_init(NULL);}else {g_print("thread not supported\n");}gtk_init(&argc, &argv);window = gtk_window_new(GTK_WINDOW_TOPLEVEL);g_signal_connect(G_OBJECT(window), "delete_event", G_CALLBACK(on_delete_event), NULL);gtk_window_set_title(GTK_WINDOW(window), "客户端");gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);gtk_container_set_border_width(GTK_CONTAINER(window), 10);vbox = gtk_vbox_new(FALSE, 0);gtk_container_add(GTK_CONTAINER(window), vbox);hbox = gtk_hbox_new(FALSE, 0);gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 5);label = gtk_label_new("点击登录按钮连接服务器");gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);login_button = gtk_button_new_with_label("登录");gtk_box_pack_start(GTK_BOX(hbox), login_button, FALSE, FALSE, 5);g_signal_connect(G_OBJECT(login_button), "clicked", G_CALLBACK(on_login), NULL);view = gtk_scrolled_window_new(NULL, NULL);gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(view), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);text = gtk_text_view_new();gtk_box_pack_start(GTK_BOX(vbox), view, TRUE, TRUE, 5);gtk_container_add(GTK_CONTAINER(view), text);buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text));hbox = gtk_hbox_new(FALSE, 0);gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 5);label = gtk_label_new("输入信息:");gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5);message_entry = gtk_entry_new();gtk_box_pack_start(GTK_BOX(hbox), message_entry, FALSE, FALSE, 5);button = gtk_button_new_with_label("发送");gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 5);g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(on_send), NULL);gtk_widget_show_all(window);gdk_threads_enter();gtk_main();gdk_threads_leave();return TRUE;
}
运行环境:CentOS6.6
编译说明:
对server.c进行编译,打开终端,用cd切换到文件对应的路径,输入命令:
gcc -o server server.c `pkg-config --cflags --libs glib-2.0 gthread-2.0`
对client.c进行编译,打开终端,用cd切换到文件对应的路径,输入命令:
gcc -o client client.c `pkg-config --cflags --libs gtk+-2.0 gthread-2.0`
生成server和client文件,运行这两个文件,打开两个终端,输入以下两个命令:
./server
./client
注意:若自己本地电脑即作为服务器端又作为客户端,只需要将服务器端和客户端代码中IP地址都改为自己本地电脑的IP地址即可