功能介绍:
此demo是基于TCP套接字编程,目的是实现一个聊天室效果。类似于QQ群效果,如果上线可以通知其他好友,下线也会通知其他好友。
需要用的技术:
一、socket编程。
1> socket 网络编程常识:既要考虑客户端 又要考虑服务器端。
2>TCP 一对多开发步骤:
服务端:
①:创建socket,使用socket()
#include <sys/socket.h> int socket(int family, int type, int protocol);
family: 指明协议族
AF_INET: IPv4协议;
AF_INET6: IPv6协议;
AF_LOCAL: Unix域协议;
AF_ROUTE: 路由套接字;
AF_KEY: 密钥套接字;
type:指明套接字类型
SOCK_STREAM 字节流套接字(TCP)
SOCK_DGRAM 数据报套接字(UDP)
SOCK_SEQPACKET 有序分组套接字
SOCK_RAW 原始套接字
protocol:设为某个协议类型常值 一般设为 0;
② 准备通信地址,sockaddr_in:
struct sockaddr{ sa_family_t sa_family; char sa_data[]; .... .... }; struct in_addr{ in_addr_t s_addr; }; struct sockaddr_in{ sa_family_t sin_family; //协议族 要和socket中的family相同 in_port_t sin_port; //网络端口 struct in_addr sin_addr; //网络地址 };
需要注意的是在对sockaddr_in 赋值的时候需要用到两个函数:
#include <arpa/inet.h> uint16_t htons(uint16_t hostint16); //转换成网络字节序表示的16位整型数 in_addr_t inet_addr(const char *cp); //把字符串转换成32位二进制网络字节序的IPV4地址
③:绑定 套接字描述符 和 通信地址 使用函数 bind();
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
需要注意的是为使不同格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构 sockadd表示。
④:监听客户端, 使用函数 listen();
#include <sys/socket.h> int listen(int sockfd, int backlog);
⑤:等待客户端的连接,使用函数 accept(). 此函数在客户端连接上来后,将返回一个新的socket描述符,这个心得描述符用于和客户端的交互。
#include <sys/socket.h> int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
使用此函数需要注意的是,如果想从客户端那里得到一些信息需要重新声明一个 struct socketaddr_in 类型的参数,然后传给此函数的第二个和第三个参数即可。
⑥:把accept 返回的socket描述符当做文件描述符来操作即可;
#include <unistd.h> ssize_t read(int filedes, void *buf, size_t nbytes); ssize_t write(int filedes, const void *buf, size_t nbytes); ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
recv 的返回值: >0表示接受到消息的字节数; =0表示对方已经断开连接; <0 表示出错;
⑦:关闭描述符;
客户端:
①:创建socket,使用socket()
②: 准备通信地址,sockaddr_in:
③:建立连接 使用函数 connect()
#include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
④:把socketfd当做文件描述符来使用即可,就像服务端的第六步。
二、多线程技术;
编程思想:
一、首先需要两个执行程序,一个模拟客户端,一个模拟服务端
二、客户端首先使用socket connect 等函数连接上服务器,然后创建多线程用来接收服务器发来的消息,主线程用来发送消息到服务器。
三、服务器端:
①:首先创建一个全局变量数组用来保存客户端连接上来的信息,因为不止一个客户端连接上。
②:创建 socket 套接字,然后监听客户端发来的消息,当有客户端连接上来的时候创建多线程为此客户端服务。
需要注意的是对全局变量——保存所有客户端套接字的数组。当客户端断开连接的时候需要清理数组中的信息。
client.c:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <sys/socket.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <stdlib.h> 7 #include <unistd.h> 8 #include <string.h> 9 #include <signal.h> 10 //准备工作 11 int sockfd;// 12 char* IP = "127.0.0.1";//本机IP,回送地址 13 short PORT = 10222; 14 typedef struct sockaddr SA;//类型转换 15 char name[20];//客户端昵称 16 //启动客户端,连接服务器 17 void init(){ 18 printf("聊天室客户端开始启动\n"); 19 sockfd = socket(AF_INET,SOCK_STREAM,0); 20 struct sockaddr_in addr; 21 addr.sin_family = AF_INET; 22 addr.sin_port = htons(PORT); 23 addr.sin_addr.s_addr = inet_addr(IP); 24 if(connect(sockfd,(SA*)&addr,sizeof(addr))==-1){ 25 perror("无法连接到服务器"); 26 printf("客户端启动失败\n"); 27 exit(-1); 28 } 29 printf("客户端启动成功\n"); 30 } 31 //开始通信 32 void* recv_thread(void* p){//收消息 33 while(1){ 34 char buf[100] = {0}; 35 if(recv(sockfd,buf,sizeof(buf),0)<=0){ 36 return; 37 } 38 printf("%s\n",buf); 39 } 40 } 41 void start(){ 42 //发送消息 43 //发消息之前,启动一个线程,用来接受服务器发送过来的消息 44 pthread_t pid; 45 pthread_create(&pid,0,recv_thread,0); 46 while(1){ 47 char buf[100] = {0}; 48 scanf("%s",buf);//接受用户输入 49 char msg[100] = {0}; 50 sprintf(msg,"%s 说:%s",name,buf); 51 send(sockfd,msg,strlen(msg),0);//发给服务器 52 } 53 } 54 void sig_close(){ 55 //关闭客户端的描述符 56 close(sockfd); 57 exit(0); 58 } 59 int main(){ 60 signal(SIGINT,sig_close);//关闭CTRL+C 61 printf("请输入您的昵称:"); 62 scanf("%s",name); 63 init();//连接服务器 64 send(sockfd,name,strlen(name),0);//将昵称发给服务器 65 start();//开始通信 66 return 0; 67 }
server.c:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <sys/socket.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <stdlib.h> 7 #include <unistd.h> 8 #include <string.h> 9 #include <signal.h> 10 //准备工作 11 int sockfd;// 12 char* IP = "127.0.0.1";//本机IP,回送地址 13 short PORT = 10222; 14 typedef struct sockaddr SA;//类型转换 15 struct client{// 16 char name[20];//存储客户昵称 17 int fds;//客户端socket描述符 18 }; 19 struct client c[100] = {0};//最多记录100个链接到服务器的客户端 20 int size = 0;//记录客户端的个数,数组的索引 21 //初始化服务器的网络,创建socket 22 void init(){ 23 printf("聊天室服务器开始启动..\n"); 24 sockfd = socket(AF_INET,SOCK_STREAM,0); 25 if(sockfd == -1){ 26 perror("创建socket失败"); 27 printf("服务器启动失败\n"); 28 exit(-1); 29 } 30 //准备网络通信地址 31 struct sockaddr_in addr; 32 addr.sin_family = AF_INET; 33 addr.sin_port = htons(PORT); 34 addr.sin_addr.s_addr = inet_addr(IP); 35 if(bind(sockfd,(SA*)&addr,sizeof(addr))==-1){ 36 perror("绑定失败"); 37 printf("服务器启动失败\n"); 38 exit(-1); 39 } 40 printf("成功绑定\n"); 41 //设置监听 42 if(listen(sockfd,10)==-1){ 43 perror("设置监听失败"); 44 printf("服务器启动失败\n"); 45 exit(-1); 46 } 47 printf("设置监听成功\n"); 48 printf("初始化服务器成功\n"); 49 //等待客户端链接,放到另一个函数中 50 } 51 //线程函数,用来接受客户端的消息,并把消息发给所有客户端 52 //分发消息函数 53 void sendMsgToAll(char* msg){ 54 int i = 0; 55 for(;i<size;i++){ 56 printf("sendto%d\n",c[i].fds); 57 send(c[i].fds,msg,strlen(msg),0); 58 } 59 } 60 void* service_thread(void* p){ 61 int fd = *(int*)p;//拿到标记客户端的sockfd 62 printf("pthread=%d\n",fd);//输出测试 63 //记录客户端的sockfd 64 c[size].fds = fd; 65 char name[20] = {0}; 66 if(recv(fd,name,sizeof(name),0)>0){ 67 strcpy(c[size].name,name);//拿到昵称 68 } 69 size++; 70 char tishi[100] = {0}; 71 //群发通知消息 72 sprintf(tishi,"热烈欢迎 %s 登录聊天室..",name); 73 //发给所有人 74 sendMsgToAll(tishi); 75 while(1){ 76 char buf[100] = {0}; 77 if(recv(fd,buf,sizeof(buf),0)==0){ 78 //返回0,表示TCP另一端断开链接 79 //有客户端退出 80 printf("fd=%dquit\n",fd);//测试 81 int i; 82 char name[20] = {0}; 83 for(i=0;i<size;i++){ 84 if(c[i].fds == fd){ 85 strcpy(name,c[i].name); 86 c[i].fds = c[size-1].fds; 87 strcpy(c[i].name,c[size-1].name);//用最后一个有效的数组元素,覆盖当前没用的这个保存退出的客户端的信息的数组元素 88 } 89 } 90 size--; 91 printf("quit->fd=%dquit\n",fd); 92 char msg[100] = {0}; 93 sprintf(msg,"欢送 %s 离开聊天室,再见!",name); 94 //群发退出通知 95 sendMsgToAll(msg); 96 close(fd); 97 return;//客户端退出了,结束服务线程 98 } 99 sendMsgToAll(buf);//接受成功,广播聊天信息 100 } 101 } 102 //等待客户端的连接,启动服务器的服务 103 void service(){ 104 printf("服务器开始服务\n"); 105 while(1){ 106 struct sockaddr_in fromaddr; 107 socklen_t len = sizeof(fromaddr); 108 int fd = accept(sockfd,(SA*)&fromaddr,&len); 109 if(fd == -1){ 110 printf("客户端链接出错\n"); 111 continue;//继续循环,处理连接 112 } 113 //如果客户端成功连接上 114 printf("fd=%d\n",fd);//测试 115 //启动线程 116 pthread_t pid; 117 pthread_create(&pid,0,service_thread,&fd); 118 } 119 } 120 void sig_close(){ 121 //关闭服务器的socket 122 close(sockfd); 123 printf("服务器已经关闭\n"); 124 exit(0); 125 } 126 int main(){ 127 signal(SIGINT,sig_close);//退出CTRL+C 128 init(); 129 service(); 130 return 0; 131 }
以上的代码是老师写的,老师用的是数组来保存所有的客户端的信息。但是自己在做这个程序的时候想法有点偏差,我用的是动态分配的链表技术来保存连接上来得客户端的socket 套接字,动态分配,动态删除。代码如下:
client.c:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include <stdio.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <stdlib.h> 7 #include <string.h> 8 9 struct info{ 10 char name[10]; 11 char buf[100]; 12 }; 13 14 //利用多线程接收发来的信息,可以在任何时候接收信息,包括发信息的时候 15 void* getmssage(void* p){ 16 int* sockfd = (int*)p; 17 struct info from; 18 while(recv(*sockfd,&from,sizeof(from),0) > 0){ 19 printf("%s说:%s\n",from.name,from.buf); 20 memset(from.name,0,sizeof(from.name)); 21 memset(from.buf,0,sizeof(from.buf)); 22 } 23 } 24 25 int main(){ 26 int sockfd = socket(AF_INET,SOCK_STREAM,0); 27 if(sockfd == -1) perror("socket"),exit(-1); 28 struct sockaddr_in addr; 29 addr.sin_family = AF_INET; 30 addr.sin_port = htons(2222); 31 addr.sin_addr.s_addr = inet_addr("172.30.13.70"); 32 33 int res = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr)); 34 if(res == -1) perror("connect"),exit(-1); 35 36 struct info in; 37 printf("请输入用户名:"); 38 scanf("%s",in.name); 39 getchar(); //此时必须要用getchar 因为第 43 行用的是gets接收数据, 38行的scanf函数接收数据的时候碰到 \n就结束了,下面的gets 就会把缓存区的 \n 给接收到, 如果下面用 scanf 接收数据的话,此时就不需要用 getchar了, 因为 scanf 在第一个字符碰到 \n的时候会直接把 \n 给忽略掉 40 pthread_t pid; 41 pthread_create(pid,0,getmssage,(void*)&sockfd); 42 while(1){ 43 //scanf("%s",in.buf); 44 gets(in.buf); 45 if(!strcmp(in.buf,"bye")){ 46 strcpy(in.buf,"退出登陆"); 47 send(sockfd,&in,sizeof(in),0); 48 exit(0); 49 } 50 if(send(sockfd,&in,sizeof(in),0) == -1) perror("send"),exit(-1); 51 memset(in.buf,0,strlen(in.buf)); 52 } 53 }
server.c:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/socket.h> 4 #include <netinet/in.h> 5 #include <arpa/inet.h> 6 #include <string.h> 7 #include <sys/types.h> 8 /* 9 int accfd[10]; 10 static int i = 0; 11 int pthid[10]; 12 */ 13 struct info{ 14 int accfd; 15 int pthid; 16 int num; 17 struct info* next; 18 }; 19 20 struct clinfo{ 21 char name[10]; 22 char buf[100]; 23 }; 24 25 static int i = 0; 26 struct info* head; 27 28 void* del(int n){ 29 struct info *p; 30 struct info *t; 31 32 t = head; 33 p = head->next; 34 printf("del n:%d\n",n); 35 while(p){ 36 if(p->num == n){ 37 t->next = p->next; 38 free(p); 39 break; 40 } 41 p = p->next; 42 t = t->next; 43 } 44 45 printf("after free\n"); 46 t = head->next; 47 while(t){ 48 printf("t->num:%d\n",t->num); 49 t = t->next; 50 } 51 } 52 53 void* message(void* p){ 54 printf("this is message\n"); 55 int num = (int)p; 56 struct info* t = head; 57 t = t->next; 58 59 int accfd; 60 int pthid; 61 62 while(t){ 63 if(t->num == num){ 64 accfd = t->accfd; 65 pthid = t->pthid; 66 break; 67 } 68 t = t->next; 69 } 70 71 struct clinfo cin; 72 memset(cin.buf,0,sizeof(cin.buf)); 73 memset(cin.name,0,sizeof(cin.name)); 74 while(1){ 75 while(recv(accfd,&cin,sizeof(cin),0) > 0){ 76 printf("%s say: %s\n",cin.name,cin.buf); 77 t = head->next; 78 while(t){ 79 printf("t->num:%d,num:%d\n",t->num,num); 80 if(t->num != num){ 81 send(t->accfd,&cin,sizeof(cin),0); 82 } 83 t = t->next; 84 } 85 if(!strcmp(cin.buf,"退出登陆")){ 86 del(num); 87 printf("即将退出线程\n"); 88 close(accfd); 89 sleep(1); 90 pthread_exit(0); 91 } 92 memset(cin.buf,0,sizeof(cin.buf)); 93 memset(cin.name,0,sizeof(cin.name)); 94 } 95 } 96 } 97 98 struct info* init(){ 99 struct info* in; 100 in = (struct info*)malloc(sizeof(struct info)); 101 in->next = NULL; 102 return in; 103 } 104 105 int main(){ 106 int sockfd = socket(AF_INET,SOCK_STREAM,0); 107 if(sockfd == -1) perror("socket"),exit(-1); 108 struct sockaddr_in addr; 109 addr.sin_family = AF_INET; 110 addr.sin_port = htons(2222); 111 addr.sin_addr.s_addr = inet_addr("172.30.13.70"); 112 113 //解决地址被占用问题 114 int reuse = 10; 115 setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)); 116 //绑定 117 int res = bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)); 118 if(res == -1) perror("bind"),exit(-1); 119 //监听 120 listen(sockfd,100); 121 struct sockaddr_in from; 122 socklen_t len = sizeof(from); 123 124 head = init(); 125 while(1){ 126 //得到新的描述符 127 int accfd = accept(sockfd,(struct sockaddr*)&from,&len); 128 struct info* p = head; 129 struct info* tmp = init(); 130 //让指针指向链表的尾部 131 while(p->next){ 132 p = p->next; 133 } 134 p->next = tmp; 135 136 tmp->num = i++; 137 tmp->accfd = accfd; 138 printf("%s连接上来\n",inet_ntoa(from.sin_addr)); 139 //创建线程 140 pthread_create(&(tmp->pthid),0,message,(void*)(i-1)); 141 } 142 143 }