一.linux-tcp通信框架
1.基础框架
1.1 tcp 服务器框架
1.套接字
#include int socket(int domain, int type, int protocol);
返回的文件描述符可以指向当前的socket,后续通过对文件描述符的访问就可以配置这个socket
成功时返回文件描述符,失败时返回-1。
●domain 套接字中使用的协议族(ProtocolFamily)信息。
●type 套接字数据传输类型信息。((SOCK_STREAM)---TCP,(SOCKDGRAM)---UDP)
●protocol 计算机间通信中使用的协议信息。
2.bind 函数
如果把套接字比喻为电话,那么创建套接字只安装了电话机。 接着就要给电话机分配号码的方法,即给套接字分配 IP地址和端口号。就是用的bind函数。
#include int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
参数一:套接字描述符 sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符。
参数二:myaddr 存有地址信息的结构体变量地址值
struct sockaddr { unsigned short sa_family; //2 char sa_data[14]; //14 };
struct in_addr { In_addr_t s_addr; //32 位 IPv4 地址 }; struct sockaddr_in { sa_family//地址族(Address Family) sa_family_t sin_family;// //地址族(Address Family) uint16_t sin_port; // 16 位 TCP/UDP 端口号 struct in_addr sin_addr; //32 位 IP 地址 char sin_zero[8]; //不使用 }
这里我们使用scokaddr_in来配置端口和ip(参数填写更方便),然后转换为socketadrr就行,两个结构体可以互相转换的
参数三:第二个结构体变量的长度
示例配置如下:
struct sockaddr_in addr; char* serv_ip="211.217.168.13"; //声明 IP地址字符串 char * serv_port="9198"; //声明端口号字符串 memset(&addr,0,sizeof(addr);//结构体变量 addr 的所有成员初始化为 0 //指定地址族 addr.sin_family =AF_INET; //基于字符串的IP地址初始化 //inet用于将点分十进制的IP地址字符串转换成网络字节顺序(big-endian)的整数表示形式。 addr.sin_addr.s_addr=inet_addr(serv_ip); //基于字符串的端口号初始化 //atoi字符型转换为整型 addr.sin_port=htons(atoi(serv_port));
3.listen 函数:
#include int listen(int sock,int backlog);
sock 希望进入等待连接请求状态的套接字文件描述符
- 传递的描述符套接字参数成为服务器端 套接字(监听套接字)。
- backlog 连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连 接请求进入队列。
4.accept 函数:
#include int accept(int sock,struct sockaddr * addr, socklen_t*addrlen);
成功时返回创建的套接字文件描述符,失败时返回-1。
- 参数sock:服务器套接字的文件描述符。
- 参数addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
- 参数addrlen:第二个参数结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填客户端地址长度。
accept 函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。套接字是自动创建的,并自 动与发起连接请求的客户端建立连接。上图展示了accept函数调用过程。
5. tcp 服务端框架
#include #include #include #include #include #include int main(){ int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; /* 在网络编程中,一个服务器可能有多个网络接口(即多个IP地址)。如果你指定了一个具体的 IP地址来绑socket,那么服务器程序只能接受发送到这个特定IP地址的连接请求。相反, 使用INADDR_ANY可以让服务接受到达服务器上任何网络接口的连接请求, 这样就不需要针对每个可能的IP地址分别设置监听了。 */ serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(9190); bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(serv_sock, 20); //用于生成客户端套接字,中间需要使用一个addr结构体,我们创建一个空结构体传入 accept辅助完成socket创建 struct sockaddr_in clnt_addr; socklen_t clnt_addr_size = sizeof(clnt_addr); //accept后会创建一个套接字, int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size); char message[] = "Hello, World!"; write(clnt_sock, message, sizeof(message)); close(clnt_sock); close(serv_sock); return 0; }
1.2 tcp 客户端框架
1.connect
#include int connect(int sock,struct sockaddr*servaddr, socklen_t addrlen);
成功时返回0,失败时返回-1。
- sock 客户端套接字文件描述符。
- servaddr 保存目标服务器端地址信息的变量地址值。
- addrlen 以字节为单位传递给第二个结构体参数servaddr的地址变量长度。
2.客户端框架
#include #include #include #include #include #include int main(){ int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); serv_addr.sin_port = htons(9190); // connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); char buffer[40]; read(sock, buffer, sizeof(buffer)-1); printf("Message from server: %s\n", buffer); close(sock); return 0; }
1.3 线程基础
1.线程创建
#include int pthread_create( pthread_t*restrict thread, const pthread_attr_t * restrict attr, void *(* start_routine)(void *), void*restrict arg );
成功时返回 0,失败时返回其他值。
●thread:保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID。
●attr:用于传递线程属性的参数,传递NULL时,创建默认属性的线程。
●start_routine:相当于线程main函数的、在单独执行流中执行的函数地址值(函数指针)。
●arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值。
#include int pthread_join(pthread_t thread, void ** status);
调用pthread_join 函数的进程(或线程)将进入等待状态,直到第一个参数为ID的线程终 止为止。而且可以得到线程的main函数返回值,所以该函数比较有用。下面通过示例了解 该函数的功能。
成功时返回e,失败时返回其他值。
- thread 该参数值ID的线程终止后才会从该函数返回。
- status保存线程的main函数返回值的 指针变量地址值。
2.互斥锁
#include int pthread_mutex_init(pthread mutex_t*mutex, const pthread_mutexattr_t* attr); int pthread_mutex_destroy(pthread_mutex_t * mutex);
- mutex 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
- attr传递即将创建的互斥量属性,没有特别需要指定的属性时传递NULL。
#include int pthread_mutex_lock(pthread_mutex_t* mutex); int pthread_mutex_unlock(pthread mutex_t* mutex);
成功时返回0,失败时返回其他值。
使用方法:
pthread_mutex_lock(&mutex); //临界区的开始 //..... // 临界区的结束 pthreadmutex_unlock(&mutex);
线程使用实例:
#include #include #include // 全局变量作为共享资源 int shared_resource = 0; // 互斥量对象 pthread_mutex_t mutex; // 线程函数,递增共享资源 void* thread_func(void* arg) { for(int i = 0; i
二、并发服务器
2.1 多线程服务器实现
1.服务器端:
每次accept都阻塞,来一个连接,就创建一个线程进行处理,多线程互不干扰
#include #include #include #include #include #include #include #include #define BUF_SIZE 100 #define MAX_CLNT 256 void* handle_clnt(void* arg); void send_msg(char* msg, int len); void error_handling(char* msg); int clnt_cnt = 0; int clnt_socks[MAX_CLNT]; pthread_mutex_t mutx; int main(int argc, char* argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; int clnt_adr_sz; pthread_t t_id; if (argc != 2) { printf("Usage : %s \n", argv[0]); exit(1); } pthread_mutex_init(&mutx, NULL); serv_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if (listen(serv_sock, 5) == -1) error_handling("listen() error"); while (1) { clnt_adr_sz = sizeof(clnt_adr); clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz); pthread_mutex_lock(&mutx); clnt_socks[clnt_cnt++] = clnt_sock; pthread_mutex_unlock(&mutx); pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock); pthread_detach(t_id); printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr)); } close(serv_sock); return 0; } void* handle_clnt(void* arg) { int clnt_sock = *((int*)arg); int str_len = 0, i; char msg[BUF_SIZE]; while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0) send_msg(msg, str_len); pthread_mutex_lock(&mutx); for (i = 0; i
2.客户端:
一个主线程即可
#include #include #include #include #include #include #include #define BUF_SIZE 100 #define NAME_SIZE 20 void* send_msg(void* arg); void* recv_msg(void* arg); void error_handling(char* msg); char name[NAME_SIZE] = "[DEFAULT]"; char msg[BUF_SIZE]; int main(int argc, char* argv[]) { int sock; struct sockaddr_in serv_addr; pthread_t snd_thread, rcv_thread; void* thread_return; if (argc != 4) { printf("Usage : %s \n", argv[0]); exit(1); } sprintf(name, "[%s]", argv[3]); sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) error_handling("connect() error"); pthread_create(&snd_thread, NULL, send_msg, (void*)&sock); pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock); pthread_join(snd_thread, &thread_return); pthread_join(rcv_thread, &thread_return); close(sock); return 0; } void* send_msg(void* arg) { int sock = *((int*)arg); // send thread main char name_msg[NAME_SIZE + BUF_SIZE]; while (1) { } fgets(msg, BUF_SIZE, stdin); if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) { close(sock); exit(0); } sprintf(name_msg, "%s %s", name, msg); write(sock, name_msg, strlen(name_msg)); return NULL; } void* recv_msg(void* arg) { int sock = *((int*)arg); // read thread main char name_msg[NAME_SIZE + BUF_SIZE]; int str_len; while (1) { str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1); if (str_len == -1) return (void*)-1; name_msg[str_len] = 0; fputs(name_msg, stdout); } return NULL; } void error_handling(char* msg) { fputs(msg, stderr); fputc('\n', stderr); exit(1); }
2.2 select 多路io复用实现
1. 原理及步骤
文件描述符:
一开始默认是空的,当我们创建socket或者文件时,就会从3开始分配文件描述符给相应的socket或者文件
2.select 函数 与fd_set
select:
#include#include int select(int maxfd, fd_set*readset, fd_set* writeset, fd_set*exceptset, const struct timeval * timeout);
成功时返回大于0的值,失败时返回-1。
- maxtfd :监视对象文件描述符数量
- readset:用于检查可读性
- writeset:用于检查可写性
- exceptset:用于检查带外数据
- timeout:一个指向timeval 结构的指针,用于决定select等待I/O的最长时间。如果为空将 一直等待。
fd_set结构体:
作用:用于表示一组文件描述符的集合,不要和文件描述符结构混淆
- FD_ZERO(fdset*fdset)∶将 fdset 变量的所有位初始化为0。
- FD_SET(int fd,fd set*fdset)∶在参数 fdset 指向的变量中注册文件描述符fd的信息。
- FD_CLR(int fd,fdset*fdset)∶从参数 fdset 指向的变量中清除文件描述符fd的信息。
- FD_ISSET(int fd,fd_set*fdset)∶若参数 fdset 指向的变量中包含文件描述符fd的信息,则 返回"真"。
3.select实现并发服务器
主要步骤:
1.由于serv_sock是最开始创建的,那么它一定是最大的文件描述符,且只创建了 serv_sock,我们将这个文件描述符添加到fd_map中(具有能够被select监听的资格,但这个监听和listen不是一个东西,如果相应文件描述符表示的sock有动作,那么select就会结束阻塞,开始后面的工作):
FD_ZERO(&reads); FD_SET(serv_sock, &reads); fd_max = serv_sock;
2. 进入循环,我们将当前的所有得到的文件描述符都放入进行监听
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break; if (fd_num == 0) continue;
3.遍历到最大文件描述符,通过FD_SET来判断哪些文件描述符是被激活的(导致select不阻塞的),同时记得处理时,要把这个文件描述符clear掉
for (i = 0; i
完整代码如下:
#include #include #include #include #include #include #include #include #define BUF_SIZE 100 void error_handling(char* buf); int main(int argc, char* argv[]) { int serv_sock, clnt_sock; struct sockaddr_in serv_adr, clnt_adr; struct timeval timeout; fd_set reads, cpy_reads; socklen_t adr_sz; int fd_max, str_len, fd_num, i; char buf[BUF_SIZE]; if (argc != 2) { printf("Usage : %s \n", argv[0]); exit(1); } serv_sock = socket(PF_INET, SOCK_STREAM, 0); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); serv_adr.sin_port = htons(atoi(argv[1])); if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) error_handling("bind() error"); if (listen(serv_sock, 5) == -1) error_handling("listen() error"); FD_ZERO(&reads); FD_SET(serv_sock, &reads); fd_max = serv_sock; while (1){ cpy_reads = reads; timeout.tv_sec = 5; timeout.tv_usec = 5000; if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) break; if (fd_num == 0) continue; for (i = 0; i
4.客户端
客户端代码和一般客户端无异:
#include #include #include #include #include #include #define BUF_SIZE 1024 void error_handling(char* message); int main(int argc, char* argv[]) { int sock; char message[BUF_SIZE]; int str_len; struct sockaddr_in serv_adr; if (argc != 3) { printf("Usage : %s \n", argv[0]); exit(1); } sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) error_handling("socket() error"); memset(&serv_adr, 0, sizeof(serv_adr)); serv_adr.sin_family = AF_INET; serv_adr.sin_addr.s_addr = inet_addr(argv[1]); serv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1) error_handling("connect() error!"); else puts("Connected..........."); while (1) { fputs("Input message(Q to quit): ", stdout); fgets(message, BUF_SIZE, stdin); if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break; write(sock, message, strlen(message)); str_len = read(sock, message, BUF_SIZE - 1); message[str_len] = 0; printf("Message from server: %s", message); } close(sock); return 0; } void error_handling(char* message) { fputs(message, stderr); fputc('\n', stderr); exit(1); }