C语言实现简易client/server网络多人聊天工具
一、C语言实现一个简易的client/server聊天工具
在ubuntu平台上,采用c语言实现一个简易的client/server聊天工具,思路是:
服务器端:首先创建一个服务器进程,该进程监听客户端的连接,如果收到并建立连接后创建一个线程服务该客户端。该线程负责消息的转发(这里为了方便直接对消息进行广播)。
客户端:客户端进程首先创建一个线程用于消息接收处理,然后为用户提供信息输入的交互界面。
主要调用栈:
int socket( int domain, int type, int protocol)
- 功能:创建一个新的套接字,返回套接字描述符
- 参数说明:
- domain:域类型,指明使用的协议栈,如TCP/IP使用的是 PF_INET
- type: 指明需要的服务类型, 如
- SOCK_DGRAM: 数据报服务,UDP协议
- SOCK_STREAM: 流服务,TCP协议
- protocol:一般都取0
- 举例:s=socket(PF_INET,SOCK_STREAM,0)
int connect(int sockfd,struct sockaddr *server_addr,int sockaddr_len)
- 功能: 同远程服务器建立主动连接,成功时返回0,若连接失败返回-1。
- 参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- Server_addr:指明远程端点:IP地址和端口号
- sockaddr_len :地址长度
int bind(int sockfd,struct sockaddr * my_addr,int addrlen)
- 功能:为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接
- 参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- my_addr:本地地址,IP地址和端口号
- addrlen :地址长度
- 举例:bind(sockfd, (struct sockaddr *)&address, sizeof(address));
int listen(int sockfd,int input_queue_size)
- 功能:
- 面向连接的服务器使用它将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的
- 参数说明:
- Sockfd:套接字描述符,指明创建连接的套接字
- input_queue_size:该套接字使用的队列长度,指定在请求队列中允许的最大请求数
- 举例:listen(sockfd,20)
int accept(int sockfd, void *addr, int *addrlen);
- 功能:获取传入连接请求,返回新的连接的套接字描述符。为每个新的连接请求创建了一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。
- 参数说明:
- Sockfd:套接字描述符,指明正在监听的套接字
- addr:提出连接请求的主机地址
- addrlen:地址长度
- 举例:new_sockfd = accept(sockfd, (struct sockaddr *)&address, &addrlen);
int send(int sockfd, const void * data, int data_len, unsigned int flags)
- 功能:在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1。send会将外发数据复制到OS内核中
- 参数说明:
- sockfd:套接字描述符
- data:指向要发送数据的指针
- data_len:数据长度
- flags:一直为0
- 举例(p50):send(s,req,strlen(req),0);
int recv(int sockfd, void *buf, int buf_len,unsigned int flags);
- 功能:从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃。
- 参数说明:
- Sockfd:套接字描述符
- Buf:指向内存块的指针
- Buf_len:内存块大小,以字节为单位
- flags:一般为0
- 举例:recv(sockfd,buf,8192,0)
close(int sockfd);
- 功能:撤销套接字。如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它。
- 参数说明:
- Sockfd:套接字描述符
- 举例:close(socket_descriptor)
二、服务器端源代码:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <string.h> 4 #include <sys/types.h> 5 #include <sys/socket.h> 6 #include <netinet/in.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 10 int client_sockfd[100] = {0};//客户端套接字 11 int count = 0; 12 13 void* clientThreadLoop(void* local) 14 { 15 char buf[BUFSIZ]; //数据传送的缓冲区 16 17 //printf("线程启动\n"); 18 int socket_id = *(int *)local; 19 while(1) 20 { 21 memset(buf,0x00,sizeof(buf)); 22 int len=recv(socket_id,buf,BUFSIZ,0); 23 /*接收客户端的数据并将其发送给客户端--recv返回接收到的字节数,send返回发送的字节数*/ 24 if(len > 0) 25 { 26 buf[len]='\0'; 27 printf("服务器收到消息:%s\n",buf); 28 for(int i=0;i<count;i++) 29 { 30 send(client_sockfd[i],buf,len,0); 31 } 32 } 33 } 34 } 35 36 int main(int argc, char *argv[]) 37 { 38 int server_sockfd;//服务器端套接字 39 int len; 40 struct sockaddr_in my_addr; //服务器网络地址结构体 41 struct sockaddr_in remote_addr = {0}; //客户端网络地址结构体 42 int sin_size; 43 memset(&my_addr,0,sizeof(my_addr)); //数据初始化--清零 44 my_addr.sin_family=AF_INET; //设置为IP通信 45 my_addr.sin_addr.s_addr=INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上 46 my_addr.sin_port=htons(8000); //服务器端口号 47 48 /*创建服务器端套接字--IPv4协议,面向连接通信,TCP协议*/ 49 if((server_sockfd=socket(PF_INET,SOCK_STREAM,0))<0) 50 { 51 perror("socket"); 52 return 1; 53 } 54 55 /*将套接字绑定到服务器的网络地址上*/ 56 if (bind(server_sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr))<0) 57 { 58 perror("bind"); 59 return 1; 60 } 61 62 /*监听连接请求--监听队列长度为5*/ 63 listen(server_sockfd,5); 64 65 sin_size=sizeof(struct sockaddr_in); 66 while(1) 67 { 68 /*等待客户端连接请求到达*/ 69 if((client_sockfd[count]=accept(server_sockfd,(struct sockaddr *)&remote_addr,&sin_size))<0) 70 { 71 perror("accept"); 72 return 1; 73 } 74 pthread_t tid; 75 printf("accept client %s\n",inet_ntoa(remote_addr.sin_addr)); 76 len=send(client_sockfd[count],"Welcome to my server\n",21,0);//发送欢迎信息 77 pthread_create(&tid,NULL,clientThreadLoop,&client_sockfd[count]); 78 count++; 79 } 80 81 for(int j=0;j<count;j++) 82 close(client_sockfd[j]); 83 close(server_sockfd); 84 return 0; 85 }
调用accept()后进程阻塞等待,当连接成功后返回的套接字存入client_sockfd[count]数组,通过调用pthread_create()创建线程。
三、客户端源代码:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <string.h> 4 #include <sys/types.h> 5 #include <sys/socket.h> 6 #include <netinet/in.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 10 void* recThreadLoop(void* id) 11 { 12 char buffer[BUFSIZ]; 13 int cur_id = *(int *)id; 14 while(1) 15 { 16 memset(buffer,0x00,sizeof(buffer)); 17 int len=recv(cur_id,buffer,BUFSIZ,0); 18 buffer[len]='\0'; 19 printf("received:%s\n",buffer); 20 } 21 } 22 23 int main(int argc, char *argv[]) 24 { 25 pthread_t tid; 26 int client_sockfd; 27 int len; 28 struct sockaddr_in remote_addr; //服务器端网络地址结构体 29 char buf[BUFSIZ]; //数据传送的缓冲区 30 memset(&remote_addr,0,sizeof(remote_addr)); //数据初始化--清零 31 remote_addr.sin_family=AF_INET; //设置为IP通信 32 remote_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器IP地址 33 remote_addr.sin_port=htons(8000); //服务器端口号 34 35 /*创建客户端套接字--IPv4协议,面向连接通信,TCP协议*/ 36 if((client_sockfd=socket(PF_INET,SOCK_STREAM,0))<0) 37 { 38 perror("socket"); 39 return 1; 40 } 41 42 /*将套接字绑定到服务器的网络地址上*/ 43 if(connect(client_sockfd,(struct sockaddr *)&remote_addr,sizeof(struct sockaddr))<0) 44 { 45 perror("connect"); 46 return 1; 47 } 48 printf("connected to server\n"); 49 len=recv(client_sockfd,buf,BUFSIZ,0);//接收服务器端信息 50 buf[len]='\0'; 51 printf("%s",buf); //打印服务器端信息 52 53 pthread_create(&tid,NULL,recThreadLoop,&client_sockfd); 54 55 /*循环的发送接收信息并打印接收信息--recv返回接收到的字节数,send返回发送的字节数*/ 56 while(1) 57 { 58 printf("Enter string to send:\n"); 59 scanf("%s",buf); 60 if(!strcmp(buf,"quit")) 61 break; 62 len=send(client_sockfd,buf,strlen(buf),0); 63 } 64 close(client_sockfd);//关闭套接字 65 return 0; 66 }
四、浏览器打开一个URL网址进行的步骤如下:
在DNS解析阶段,需要用到如下调用栈:
- gethostname 获得主机名
- getpeername 获得与套接口相连的远程协议地址
- getsockname 获得套接口本地协议地址
- gethostbyname 根据主机名取得主机信息
- gethostbyaddr 根据主机地址取得主机信息
- getprotobyname 根据协议名取得主机协议信息
- getprotobynumber 根据协议号取得主机协议信息
- getservbyname 根据服务名取得相关服务信息
- getservbyport 根据端口号取得相关服务信息
- getsockopt/setsockopt 获取/设置一个套接口选项
- ioctlsocket 设置套接口的工作方式
获取到域名对应的IP之后便可以开始向服务器请求连接。
五、Socket系统调用
Socket系统调用的流程:
(1)系统调用 –> (2)查找socket –> (3)执行socket的对应操作函数 –> (4)执行传输层协议的对应操作函数;
内核源码:
1 SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 2 { 3 int retval; 4 struct socket *sock; 5 int flags; 6 7 ... 8 retval = sock_create(family, type, protocol, &sock); 9 if (retval < 0) 10 goto out; 11 12 retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); 13 if (retval < 0) 14 goto out_release; 15 16 out: 17 /* It may be already another descriptor 8) Not kernel problem. */ 18 return retval; 19 20 out_release: 21 sock_release(sock); 22 return retval; 23 }
可以看到socket函数主要由sock_create和sock_map_fd这两个函数完成。