1.TCP的特点:
需要连接,使用可靠的传输协议,用于对数据安全要求较高,传输大型数据,实时性差。
2.套接字 socket
socket--应用程序与TCP/UDP通信协议的中间层。
3.TCP通信流程
4.通信的实现,先是服务端
(1)创建套接字函数 socket
(2)绑定端口IP地址 bind
这一步所要用到的结构体
(3)监听 listen
(4)如果有客户端连接就接受连接 accept
(5)之后就是接受和发送数据了
接收/读取 recv,read
写入/发送 send/write
然后是客户端
(1)连接服务器 connect
(2)之后也是通过recv,read,write,send四个函数进行消息传输了。
服务器端的代码如下:
#include <stdio.h> //#include <linux/in.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void *recv_run(void *); int main(int argc, char **argv) { //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket fail"); return -1; } //2.连接服务器 struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); saddr.sin_family=AF_INET; saddr.sin_port = htons(atoi(argv[2])); saddr.sin_addr.s_addr = inet_addr(argv[1]); int ret = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); if(ret < 0) { perror("connect fail"); return -1; } //创建线程 pthread_t id = 0; ret = pthread_create(&id, NULL, recv_run, (void*)sockfd); char buffer[1024]={0}; while(1) { scanf("%s", buffer); write(sockfd, buffer, strlen(buffer)); } close(sockfd); return 0; } void *recv_run(void *arg) { int sockfd = (int)arg; char buffer[128]; while(1) { memset(buffer, 0, 128); int ret = read(sockfd, buffer, 128); if(ret <= 0) { perror("read fail"); break; } printf("%s ", buffer); } }
客户端的代码如下
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #define MAXCLIENT 10 struct client_info { int clientfd; struct sockaddr_in addr; char userid[32]; }; struct client_info infos[MAXCLIENT];//保存客户端信息结构体数组 void *client_run(void *); int main(int argc, char **argv) { //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket fail"); exit(1); } //2.绑定 struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); saddr.sin_family = AF_INET; saddr.sin_port = htons(atoi(argv[1])); saddr.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); if(ret < 0) { perror("bind fail"); exit(1); } //3.监听 ret = listen(sockfd, 5); if(ret < 0) { perror("listen fail"); exit(1); } //4.接受连接、 struct sockaddr_in caddr; socklen_t len = sizeof(caddr); int i=0; while(1) { int clientfd = accept(sockfd, (struct sockaddr*)&caddr, &len); if(clientfd < 0) { perror("accept fail"); continue; } //保存客户端信息 while((i<MAXCLIENT) && (infos[i].clientfd != 0))i++; if(i<MAXCLIENT) { infos[i].clientfd = clientfd; infos[i].addr = caddr; } //连接成功,创建线程 pthread_t id = 0; int pret = pthread_create(&id, NULL, client_run, (void*)clientfd); if(pret < 0) { perror("pthread_create fail"); continue; } //线程分离 pthread_detach(id); } } struct client_info *get_info(int clientfd) { for(int i=0; i<MAXCLIENT; i++) { if(infos[i].clientfd == clientfd) return &infos[i]; } return NULL; } struct client_info *get_info_id(const char *userid) { for(int i=0; i<MAXCLIENT; i++) { if(strcmp(infos[i].userid,userid)==0) return &infos[i]; } return NULL; } //线程处理客户端 void *client_run(void *arg) { int clientfd = (int)arg; //读取客户端数据 char buffer[128]={0}; while(1) { memset(buffer, 0 , 128); int ret = read(clientfd, buffer, sizeof(buffer)); if(ret <= 0) { perror("客户端掉线"); struct client_info *cinfo = get_info(clientfd); if(cinfo != NULL) { //清空客户端信息 memset(cinfo, 0, sizeof(struct client_info)); } break; } //解析接收的数据是否为userid login:userid if(strstr(buffer, "login:")) { char userid[32]; sscanf(buffer, "login:%s", userid); struct client_info *cinfo = get_info(clientfd); if(cinfo != NULL) { printf("login解析:%s ", userid); strcpy(cinfo->userid, userid); continue; } } //服务器处理客户端的数据(请求)//转发数据协议TO:userid:Data 把data给userid if(strstr(buffer,"TO:")) { char userid[32]={0}; char data[128]={0}; sscanf(buffer, "TO:%[^:]:%s",userid, data); printf("%s 解析转发数据:%s %s ", buffer, userid, data); struct client_info *cinfo = get_info_id(userid); if(cinfo != NULL) { write(cinfo->clientfd, data, strlen(data)); } } } close(clientfd); }
5.多路复用机制
(1)select
select可以接听多个文件描述符,当所有文件描述符都没有响应的时候,select会处于阻塞(休眠状态),当其中一个或者多个文件描述符有响应的时候,select函数就会被唤醒。
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 参数:int nfds 最大文件描述符加1 fd_set *readfds ---读文件描述符集合 fd_set *writefds---写文件描述符集合 fd_set *exceptfds---错误文件描述符集合 struct timeval *timeout --设置超时,如果设置为NULL一直等待响应 void FD_CLR(int fd, fd_set *set); 从set集合中清除fd int FD_ISSET(int fd, fd_set *set); 判断set中响应的文件描述符是否是fd void FD_SET(int fd, fd_set *set); 把fd添加到set集合中 void FD_ZERO(fd_set *set);清空集合
通过select来实现的TCP客户端代码如下:
#include <stdio.h> //#include <linux/in.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> int init_lcd(unsigned int **mp, const char *dev) { int fd = open(dev, O_RDWR); *mp =(unsigned int *) mmap(NULL, 800*480*4, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0); return fd; } void set_color(unsigned int *mp, unsigned int color) { for(int i=0 ;i<800*480; i++) { mp[i] = color; } } void destroy_lcd(int fd, unsigned int *mp) { munmap(mp, 800*480*4); close(fd); } int main(int argc, char **argv) { //1.创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) { perror("socket fail"); return -1; } //2.连接服务器 struct sockaddr_in saddr; memset(&saddr, 0, sizeof(saddr)); saddr.sin_family=AF_INET; saddr.sin_port = htons(atoi(argv[2])); saddr.sin_addr.s_addr = inet_addr(argv[1]); int ret = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr)); if(ret < 0) { perror("connect fail"); return -1; } //定义读文件描述符集合 fd_set readfds; //lcd unsigned int *mp = NULL; int lcdfd = init_lcd(&mp, "/dev/ubuntu_lcd"); while(1) { FD_ZERO(&readfds); //把要监听的文件描述符添加到集合中 FD_SET(sockfd, &readfds); FD_SET(0, &readfds); //select ret = select(sockfd+1, &readfds, NULL, NULL, NULL);//阻塞 if(ret < 0) { perror("select fail"); } //查看是那个文件描述符有相应(有数据可读) if(FD_ISSET(0, &readfds)) { char buffer[1024]={0}; scanf("%s", buffer); write(sockfd, buffer, strlen(buffer)); } if(FD_ISSET(sockfd, &readfds)) { char buffer[128]; memset(buffer, 0, 128); int ret = read(sockfd, buffer, 128); if(ret <= 0) { perror("read fail"); break; } printf("%s ", buffer); if(strcmp(buffer,"red") ==0) { set_color(mp,0x00ff0000); }else if(strcmp(buffer, "blue")==0) { set_color(mp,0x000000ff); } } } destroy_lcd(lcdfd, mp); return 0; }
(2)epoll机制
系统提供的接口:
1.创建epoll实例(句柄)
#include <sys/epoll.h> int epoll_create(int size); 参数: size---监听文件描述符的大小 返回值:epoll句柄(类似与文件描述符), 成功>0, 失败-1
2.添加/修改/删除要监听的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 参数:int epfd---epoll句柄 int op---操作EPOLL_CTL_ADD/MOD/DEL int fd---要添加被监听的文件描述符 struct epoll_event *event----epoll事件 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; events:事件类型(EPOLLIN--输入, EPOLLOUT-输出, EPOLLERR--出错) epoll_data_t data; 设置联合体为fd 返回值:成功返回0, 失败-1
3.监听文件描述符--“阻塞”
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 参数:int epfd--epoll句柄 struct epoll_event *events---用存储触发的事件(触发的事件有可能是多个) int maxevents --events所指向空间的长度 int timeout 》0 设置多毫秒后跳出函数 ==0立即返回 ==-1一直等待直到有事件发生 返回值:>0 返回多个个文件描述符有数据可读(事件数) ==0 没有可以数据文件描述符,超时时间到没有任何文件描述符有数据可读 ==-1出错
触发方式:(1)沿边触发 EPOLLET;(2)水平触发 EPOLLEL
6.非阻塞
1.打开文件时候设置为非阻塞打开 O_NONBLOCK
2.通过fcntl函数设置非阻塞
(1) int flag = fcntl(fd, F_GETFL); (2) flag |=O_NONBLOCK (3) fcntl(fd, F_SETFL, flag);
7.心跳包:检测客户端是否在线
epoll_server_x跳包
8.协议(数据协议包----json)
键值形式---优点方便协议升级
对象{} -----对象中存储的是键值对, 多个键值对用(,)隔开
数组[] ----数组中存储的同类型数据(对象, 数组, 字符串, 数值)
键值对 key:value ---key只能字符串, value--可以是对象, 数组, 字符串, 数字
{ "clientfd": [{ "sockfd": 5, "ip": "192.168.1.44", "userid": "aaaa" }, { "sockfd": 6, "ip": "192.168.1.45", "userid": "bbbb" }] }
json数据的打包如下:
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include "cJSON.h" int main(void) { char buffer[] = "{ "clientfd": [{ "sockfd": 5, "ip": "192.168.1.44", "userid": "aaaa" }]}"; //创建一个对象{"sockfd": 5,"ip": "192.168.1.44","userid": "aaaa"} cJSON *arrayOBj = cJSON_CreateObject(); cJSON_AddItemToObject(arrayOBj,"sockfd", cJSON_CreateNumber(5)); cJSON_AddItemToObject(arrayOBj,"ip", cJSON_CreateString("192.168.1.44")); cJSON_AddItemToObject(arrayOBj,"userid", cJSON_CreateString("aaaa")); //创建一个数组[{"sockfd": 5,"ip": "192.168.1.44","userid": "aaaa"}] cJSON *array = cJSON_CreateArray(); cJSON_AddItemToArray(array, arrayOBj); //创建最外层对象{"clientfd":[{"sockfd": 5,"ip": "192.168.1.44","userid": "aaaa"}]} cJSON *root = cJSON_CreateObject(); cJSON_AddItemToObject(root,"clientfd", array); //转字符串 char *str = cJSON_Print(root); printf("%s ", str); return 0; }
这是解析上面的数据:
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include "cJSON.h" int main(void) { char buffer[] = "{ "clientfd": [{ "sockfd": 5, "ip": "192.168.1.44", "userid": "aaaa" }, { "sockfd": 6, "ip": "192.168.1.45", "userid": "bbbb" }],"my":"ddd"}"; char send_buffer[]="{ "clientfd": [{ "sockfd": 5, "ip": "192.168.201.130", "name": "name", "password": "password" }]}"; //创建一个json解析对象 cJSON *root = cJSON_Parse(buffer); //根据clientfd键获取值--》数组 cJSON *array = cJSON_GetObjectItem(root, "clientfd"); //获取数组长度 int len = cJSON_GetArraySize(array); //遍历数组--得到对象 for(int i=0; i<len; i++) { cJSON *arrayObj = cJSON_GetArrayItem(array, i); cJSON *tmp = cJSON_GetObjectItem(arrayObj, "sockfd"); printf("%d ", tmp->valueint); tmp = cJSON_GetObjectItem(arrayObj, "ip"); printf("%s ", tmp->valuestring); tmp = cJSON_GetObjectItem(arrayObj, "userid"); printf("%s ", tmp->valuestring); } printf("%s ", buffer); }
PS:哪里写错了请指正,互相学习。