前言
决定写博客是受到一位前辈的影响,对自己学习的经历一种总结。回想自己七年的大学学习生涯,到现在刚工作,还是个半吊子水平,很多东西都不精,于是想系统的记录一下成长的过程,一方面当作笔记,另一方面也可以看看到底自己是个什么水平。
好了,费话说完,说正事。年初时自己学习linux下的网络编程,曾经写过一个简单的tcp聊天软件。就翻出来当作自己的处女篇吧。
简介
linux下的网络进程间通信是基于套接字接口API实现的,即socket。原型如下:
#include
int socket(int domain, int type, int protocol);与此类似的各种API接口,在程序中应用还有很多,函数原型以及具体的参数类型以及返回值,错误处理等,参考相应的manual page,文中就不做额外介绍了。
tcp协议是一种传输层协议,工作在五层网络模型的第二层,介于应用层与IP层(网络层)之间。与UDP协议不同的是,TCP是面向连接的,因此客户端与服务器程序编写稍显复杂。大致过程如下:
程序代码地址:https://github.com/yangyinqi/chatroom
代码
首先是服务器设计,由于涉及到多客户的服务,所以选择select进行连接描述符的轮询。
int sock_init(void)
{
int serverfd, sock_len;
struct sockaddr_in server_address;
serverfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(PORTNUM);
sock_len = sizeof(server_address);
if(bind(serverfd, (struct sockaddr *)&server_address, sock_len)<0){
perror("Bind error.");
exit(EXIT_FAILURE);
}
if(listen(serverfd, 10)<0){
perror("Listen error.");
exit(EXIT_FAILURE);
}
return serverfd;
}
对socket进行初始化,设置其为IPv4因特网域,类型为面向连接的字节流。
int main(int argc, char **argv)
{
int listenfd, comfd, maxfd;
int maxi, i, com_len, client[FD_SETSIZE];
struct sockaddr_in client_address;
fd_set readfds, testfds;
int result;
char data[MAXSIZE], str[64];
ClientPtr clientlist;
memset(data, 0, MAXSIZE);
listenfd = sock_init();
clientlist = ClientListInit();//Initial the client list
maxfd = listenfd;
maxi = -1;
for(i=0; i < FD_SETSIZE; ++i)
client[i] = -1;
FD_ZERO(&readfds);
FD_SET(listenfd, &readfds);
while(1){
int fd, nread;
testfds = readfds;
result = select(maxfd+1, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0);
if(result < 1){
perror("server5");
exit(EXIT_FAILURE);
}
if(FD_ISSET(listenfd, &testfds)){
com_len = sizeof(client_address);
comfd = accept(listenfd, (struct sockaddr *)&client_address, &com_len);
FD_SET(comfd, &readfds);
ClientAdd("Yang", comfd,
ntohs(client_address.sin_port),
inet_ntop(AF_INET, &client_address.sin_addr, str ,sizeof(str)),
clientlist);
printf("new client: %s, port %d\n",
inet_ntop(AF_INET, &client_address.sin_addr, str ,sizeof(str)),
ntohs(client_address.sin_port));
for(i=0; i
if(client[i] < 0){
client[i] = comfd;
break;
}
}
if(i == FD_SETSIZE){
perror("too many clients");
exit(EXIT_FAILURE);
}
FD_SET(comfd, &readfds);
if(comfd > maxfd)
maxfd = comfd;
if(i>maxi)
maxi = i;
}
for(i = 0; i <= maxi; ++i){
if(client[i] < 0)
continue;
if(FD_ISSET(client[i], &testfds)){
if((nread=read(client[i], data, MAXSIZE)) == 0){
close(client[i]);
FD_CLR(client[i], &readfds);
printf("client %d removed.\n", client[i]);
client[i] = -1;
}
else{
printf("client %d :%s",client[i], data);
write(client[i], data, nread);
}
}
}
}
exit(EXIT_SUCCESS);
}
本来设计了一个表来实现聊天用户名称,IP等信息的存储,但还是没有完成。
while(1)大循环里的逻辑大致是:对整个文件描述符集合进行轮询,若 监听 套接字有活动,则说明有新客户端请求连接,将信息保存后,从accept返回的fd保存在数组里。
若不是监听套接字的活动,则对保存客户端连接的套接字描述符数组进行遍历,根据read返回的值处理。
客户端:
#include "client.h"
int main(int argc, char **argv)
{
int client_len;
struct sockaddr_in client_address;
int num;
char data[MAXSIZE], readbuff[MAXSIZE];
ClientPtr local_info = (ClientPtr)malloc(sizeof(ClientNode));
//pthread_t thread_id;
void *thread_res;
int opt, error_status = 0;
if(argc < 2) {
print_notice();
exit(EXIT_FAILURE);
}
else if(argc > 3) {
printf("Too many argument");
exit(EXIT_FAILURE);
}
memset(local_info, 0, sizeof(ClientNode));
while((opt = getopt(argc, argv, ":u:n")) != -1) {
switch(opt) {
case 'u':
printf("Anonymous user.\n");
//local_info.ClientName = "Anonymous";
strcpy(local_info->ClientName, "Anonymous");
break;
case 'n':
printf("Your name: %s\n", optarg);
strcpy(local_info->ClientName, optarg);
break;
case '?':
printf("Unknown option: %c\n",optopt);
//print_notice();
error_status = 1;
break;
}
}
if(error_status) {
print_notice();
exit(EXIT_FAILURE);
}
fd_status = 0;
//memset(local_info, 0, sizeof(ClientNode));
memset(&client_address, 0, sizeof(struct sockaddr_in));
(void)signal(SIGINT, quit_sig);
client_fd = socket(AF_INET, SOCK_STREAM, 0);
client_address.sin_family = AF_INET;
client_address.sin_addr.s_addr = htonl(INADDR_ANY);
client_address.sin_port = htons(PORTNUM);
client_len = sizeof(client_address);
info_copy(local_info, &client_address);
local_info->ClientFD = client_fd;
// if((bind(client_fd, (struct sockaddr *)&client_address, client_len))<0) {
// perror("Bind error.");
// exit(EXIT_FAILURE);
// }
if((connect(client_fd, (struct sockaddr *)&client_address, client_len))<0) {
perror("Connect error.");
exit(EXIT_FAILURE);
}
if(pthread_create(&thread_id, NULL, write_thread, (void *)client_fd) != 0) {
perror("Thread creation failed.");
exit(EXIT_FAILURE);
}
while(1) {
if(fd_status) {
printf("Press enter to close");
break;
}
else {
read(client_fd, readbuff, MAXSIZE);
printf("%s", readbuff);
}
}
pthread_join(thread_id, &thread_res);
close(client_fd);
//printf("%s", (char *)thread_res);
free(local_info);
exit(EXIT_SUCCESS);
}
char *package_write(char *src)
{
return src;
}
void info_copy(ClientPtr info, struct sockaddr_in *local)
{
char addr[16];
info->ClientAddr = inet_ntop(AF_INET, &local->sin_addr, addr,sizeof(addr));
info->ClientPort = local->sin_port;
}
void print_notice()
{
printf("Option:\n\t./client -u\tConnect to the server in anonymous mode\n");
printf("\t./client -n [name]\tConnect to the server in your own name\n");
}
void *write_thread(void *arg)
{
int fd = (int)arg;
char *data_write;
data_write = (char *)malloc(MAXSIZE*sizeof(char));
//pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
//pthread_detach(pthread_self());
while(1) {
if(fd_status) {
printf("Press Enter to close");
//fflush(stdin);
break;
}//connection closed
else {
fgets(data_write, MAXSIZE, stdin);
data_write = package_write(data_write);
//pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
//pthread_testcancel();//cancle the thread when the socket closed.
send(fd, data_write, MAXSIZE, NULL);
}
}
free(data_write);
//printf("Write thread end.\n");
pthread_exit("Write thread end.\n");
}
void quit_sig(int sig)
{
printf("Press Enter to close");
// write(client_fd, client_end, 21);
fd_status = 1;
// close(client_fd);
//pthread_cancel(thread_id);
// (void)signal(SIGINT, SIG_IGN);
// exit(EXIT_SUCCESS);
}
客户端程序将发送和接受两部分分别在两个线程中进行处理。
大概的逻辑是:初始化socket以及结构体后,进行conncet,发送连接服务器的请求,返回正确后,将描述符作为参数传递给新建立的发送数据线程。相比较服务器,逻辑简单很多。至于那么多注释。。无视掉吧,想写个结束线程的东西,水平有限当时怎么也没弄出来。
后记
服务器设计还有许多思路,例如使用多线程,并发处理各个客户请求,这就需要一些线程同步机制,还有和select系统调用看起来类似,但实质上完全不同的epoll,采用回调函数的方式,对各个连接进行处理,这种方法在实际服务器开发中用的更多一些。
其实我感觉linux下的网络聊天室采用UDP套接字设计会简单一些。常用的聊天工具,比如QQ,飞秋都是基于udp协议设计自己的应用层协议的。
欢迎各路大神批评指正。最近在写一个网口传输速率的测试程序,完成后也会写成博客与大家分享!