Linux TCP多线程服务器

avatar
作者
筋斗云
阅读量:0

一、多线程开发

线程和进程

程序写好存储在硬盘介质里,CPU读取程序到内存 ,这个在内存中的可执行程序实例就叫进程。一个程序如果多次读取到内存中,那他们就是各自独立的进程
内存中的任何位置都有相应的地址方便访问,而在内存中的进程,自己内部都有一个虚拟独立的地址空间。
每个进程首先有加载的程序,通常只有一个程序计数器,用来记录当前程序执行的位置,会按照程序顺序计算,这里的一个执行流就是一个线程。如果有多个线程,就需要多个程序计数器,每个线程会独立运行。除此之外,每个进程还有寄存器、堆栈等程序运行时的状态信息,同时线程间共享的则有地址空间、全局变量、打开的文件等等信息。

线程共享的内存地址空间
在大多数现代操作系统中,当创建新的线程时,这些线程会共享进程的大部分内存地址空间。这包括全局变量、静态变量、堆(heap)上的动态分配内存等。由于这些内存区域是共享的,因此一个线程对这些区域中数据的修改对其他线程是可见的(当然,这取决于具体的同步机制,比如锁的使用)。

每个线程私有的栈空间
尽管线程共享大部分内存地址空间,但每个线程都有自己独立的栈(stack)空间。栈是用于存储局部变量、函数参数、返回地址等的地方。每个线程在执行时,都会在其私有的栈上分配和释放内存。这种设计保证了线程之间的隔离性,即一个线程无法直接访问另一个线程的栈内存(除非通过指针或其他形式的内存共享机制,但这通常是不推荐的做法,因为它会引入复杂性和潜在的风险)。

那么为什么进程中还要有更小的”进程“ - 线程 

假如进程是一个文档编辑器,存放着相应的程序和文档,现在用户用键盘敲下回车,交互的程序接收键盘的按下事件,布局的程序将文字重新排布渲染出来,另外每隔一段时间,写入的程序保存文档到硬盘中,所以这三个程序最好能并行执行,但他们又需要访问修改同一个文档,所以肯定是在同一个进程中,所以现在需要更轻量级的3个线程,交互线程, 渲染线程,保存线程。
因此,线程是并行的最小单位,假如计算机只有一个单核CPU,即同一时刻只能执行一个线程,对每个线程快速切换轮流执行。为了简化,CPU在内核中为每个线程提供各自虚拟的CPU,每个线程会认为自己独占着CPU,他们就不需要考虑时间片轮转的问题了。
一句人尽皆知的终结:进程是资源分配的最小单位,线程是CPU调度/程序执行的最小单位

并行和并发

假设有多个线程(CPU需要执行的任务),CPU的一个核心挨个挨个处理叫批处理。通过时间片轮转的方式快速切换,就是并发(多线程处理),虽然CPU切换速度非常快宏观上看起来多个线程是并行运行的,但实际不是。批处理和并发的实际处理时间是一样的
而现代的多核系统下,假设系统中有两个核心有两个线程,那么此时操作系统可以把两个线程分配给两个CPU核心,那么此时,这两个线程就可以同时进行了,这就是并行

并行和并发的区别就是在于是否一个核心还是多个核心吗?
实际上更加复杂,上述内容有一个前提就是假设线程之间是独立的,现在假设系统中有两个核心有两个线程,这两个线程需要持有同一把锁,拿到锁的线程才能向前运行,所以此时即使有两个核心也无法做到并行。这也是为什么高性能程序要避免使用锁。
所以说并行和并发不是说单核就是并发,多核就是并行,还要看线程之间的依赖关系。只有在多核,且任务之间没有关系的情况下,才叫真正的并行。当然,多核系统下,如果线程数多余核心数,既有并行也有并发。假设两个核心1,2,4个任务a,b,c,d,ab分配给了核心1,cd分配给了核心2,此时ab就是并发,cd也是并发,a与c就可以并行,b与d也可以并行

在这里插入图片描述
网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

多线程开发

top     ps-ef  都可以查看进程 

进程
进程有独立的地址空间
Linux为每个进程创建task struct
每个进程都参与内核调度,互不影响
线程
线程没有独立的内存空间,一个进程内所有线程公用一个
线程仅仅有自己的独立栈空间
使用线程的好处
大大提高了任务切换的效率避免了额外的TLB & cache的刷新

1、创建线程 pthread_create
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 参数 pthread_t *thread: 线程id指针,存放tid的 onst pthread_attr_t *attr:	线程属性 void *(*start_routine) (void *):	线程回调函数(函数指针) void *arg:	函数参数 
#include<stdio.h> #include<pthread.h> #include <unistd.h>  int funct(int a){ 	printf("hello\n"); 	pause(); }  int main() { 	pthread_t tid; 	int ret=pthread_create(&tid,NULL,funct,NULL); 	if(ret<0){ 		printf("create thread error\n"); 	} 	sleep(2); 	pause(); //进程结束,线程也就结束了 	return 0; } 
查看线程 ps -eLf | grep progressNAme 查看自己的tid pthread_t pthread_self(void) 

在这里插入图片描述

2、线程的退出
  • 主动退出
    return 退出,
    void pthread_exit(void* retval) //类似于exit(),直接可以NULL
  • 被动退出
    pthread_cancel(tid);
  • 资源回收
    int pthread_join(pthread_t thread,void **retval); 需要父进程回收,是一个阻塞函数,即会等待线程结束
    int pthread_detach(pthread_t thread);

为什么要等待退出的线程?
如果不连接已经退出的线程, 会导致资源无法释放。
所谓资源指的又是什么呢?已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。新创建的线程, 没有复用刚才退出的线程的地址空间。
如果不执行连接操作, 线程的资源就不能被释放, 也不能被复用, 这就造成了资源的泄漏

3、线程安全
4、线程同步

二、Linux多线程TCP服务器

socket函数

int socket(int domain, int type, int protocol);  创建一个通信端点,并返回一个指向该端点的文件描述符  domain :	选择用于通信的协议族。如AF_INET   :    IPv4 Internet protocols                      type:	套接字具有指定的类型,该类型指定了通信语义。 如 SOCK_STREAM  提供有序、可靠、双向、基于连接的字节流。 可支持带外数据传输机制  protocl:	指定与套接字一起使用的特定协议。 通常情况下,在给定的协议族中,只有一种协议支持特定的套接字类型, 		在这种情况下,协议可以指定为 0。然而也可能存在多种协议,在这种情况下,必须在本手册中指定特定的协议 

memset函数

void *memset(void *s, int c, size_t n); 初始化内存 The memset() function fills the first n bytes of the memory area pointed to by s with the constant byte c 

bind函数

给socket帮 ip和端口号

 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);   当使用 socket() 创建套接字时,它存在于name space中,但没有分配地址。bind() 将 addr 指定的地址分配给文件描述符 sockfd 所指向的套接字。 addrlen 指定 addr 指向的地址结构体的大小。传统上,这一操作被称为 "为套接字分配name"。  On success, zero is returned.  On error, -1 is returned 

listen函数

socket变为监听模式

int listen(int sockfd, int backlog);  listen for connections on a socket listen() 会将 sockfd 所指向的套接字标记为可连接套接字,即使用 accept() 接受传入连接请求的套接字。  sockfd :文件描述符,指向 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。  backlog :定义了 sockfd 的待处理连接队列可能增长的最大长度。  如果连接请求在队列已满时到达,客户端可能会收到 带有 ECONNREFUSED 指示的错误信息,或者,如果底层协议支持重传,该请求可能会被忽略,以便以后重新尝试连接时获得成功。 

accept函数

等待客户端连接,阻塞状态

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);   accept a connection on a socket  accept()  用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。 它从监听套接字、sockfd 的待处理连接队列中 提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套接字的新文件描述符。 新创建的套接字不处于监听状态。 原始 套接字 sockfd 不受此调用影响。  参数  sockfd :是用 socket() 创建的套接字,用 bind() 绑定到本地地址,并在 listen() 之后监听连接。   addr :指向 sockaddr 结构的指针。 该结构由通信层已知的对等套接字地址填充。 addr 返回地址的具体格式由套接字的地址系列决定。 当 addr 				        为 NULL 时,将不填写任何内容;在这种情况下,addrlen 将不被使用,也应为 NULL。  addrlen :一个值-结果参数:调用者必须初始化它,使其包含 addr 指向的结构体的大小(字节);  返回:返回一个指向该套接字的新文件描述符。它将包含对等地址的实际大小。如果提供的缓冲区太小,返回的地址将被截断;在这种情况下,addrlen 返回的值将大于调用时提供的值。  

recv函数

size_t recv(int sockfd, void *buf, size_t len, int flags);  recv, recvfrom, recvmsg - receive a message from a socket 用于接收来自套接字的信息。 它们可用于接收无连接和面向连接套接字上的数据。 recv() 与 read() 的唯一区别在于是否有标志。 如果 flags 参数为零,recv() 通常等同于 read()  如果套接字上没有可用的消息,接收调用将等待消息的到来,除非套接字是非阻塞的,在这种情况下将返回值-1,并将外部变量 errno 设为 EAGAIN 或 EWOULDBLOCK。   接收调用通常会返回任何可用的数据,最多不超过请求的数量,而不是等待收到请求的全部数据。  参数: int sockfd:已连接的套接字confd, void *buf:	接收缓冲区 size_t len:希望接收数据的最大字节长度 int flags:  返回值: return the number of bytes received, or -1 if an error occurred. 连接套接字关闭时,返回0 

实现单线程

#include <stdio.h>      // 引入标准输入输出库   #include <stdlib.h>     // 引入标准库,用于exit等函数   #include <string.h>     // 引入字符串处理库,用于strcmp等函数   #include <unistd.h>     // 引入POSIX操作系统API,用于close等函数   #include <sys/socket.h> // 引入套接字编程库   #include <netinet/in.h> // 引入IPv4和IPv6地址定义   #include <arpa/inet.h>  // 引入地址转换函数,如inet_addr      #define MAXLINE 4096    // 定义接收缓冲区的大小      int main(int argc, char** argv)   {       int listenfd, connfd; // 分别用于监听和连接的套接字文件描述符       struct sockaddr_in serveraddr; // 存储服务器地址信息的结构体       char buff[MAXLINE]; // 接收消息的缓冲区       int n, ret; // 分别用于存储接收到的字节数和比较结果          // 创建TCP套接字       //netstat   a 不仅显示套接字内容的命令,还显示尚未开始通信等状态的所有套接字      //			n 显示ip地址和端口号      // 			o 显示使用该套接字的程序PID     if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {           printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);           exit(0); // 创建失败则退出       }          // 初始化服务器地址信息       memset(&serveraddr, 0, sizeof(serveraddr));       serveraddr.sin_family = AF_INET; // 使用IPv4       serveraddr.sin_addr.s_addr = inet_addr("10.47.158.12"); // 设置服务器IP地址       serveraddr.sin_port = htons(6666); // 设置服务器端口号,htons用于主机字节序到网络字节序的转换          // 绑定套接字到服务器地址和端口       if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {           printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);           exit(0); // 绑定失败则退出       }          // 监听连接请求       if (listen(listenfd, 10) < 0) {           printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);           exit(0); // 监听失败则退出       }          printf("=========waiting for client's request.....=====\n"); // 打印等待客户端请求的消息          // 无限循环,接受并处理客户端连接       while(1) {       	//如果已建立连接,则不再调用accept,否则调用accept会生成新的connfd ,结果只会打印一次数据     	if(connfd==0){     	        if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {               		printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);                   		continue;           	}       	}          // 从连接套接字接收消息           n = recv(connfd, buff, MAXLINE, 0);           if (n <= 0) {               // 如果n <= 0,表示连接已关闭或发生错误               close(connfd);               continue;           }           buff[n] = '\0'; // 在接收到的字符串末尾添加空字符              // 比较接收到的消息是否为"exit\n"           ret = strcmp(buff, "exit\n");           if (ret == 0) {               // 如果是,则关闭监听套接字并退出循环               close(listenfd);               close(connfd); // 注意:虽然此时connfd可能不再需要,但关闭是个好习惯               break;           }           printf("recv msg from client: %s\n", buff); // 打印接收到的消息              // 关闭连接套接字(在实际应用中,可能会在此处处理更多数据或保持连接)           close(connfd);       }          // 退出循环后,关闭监听套接字(注意:在上面的"exit"情况下,这一步会被跳过)       close(listenfd);          return 0; // 程序正常退出   } 
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #include<unistd.h> #include <arpa/inet.h>  #define MAXLINE 4096  int main(int argc, char** argv) {         int listenfd, connfd;         struct sockaddr_in serveraddr;         char buff[MAXLINE];         int n, ret;          if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol                 printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);                 exit(0);         }          memset(&serveraddr, 0, sizeof(serveraddr));         serveraddr.sin_family = AF_INET; //ipv4网络类型         serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128"));  // INADDR_ANY: set addr 0.0.0.0, means all addr         serveraddr.sin_port = htons(1234);  // 绑定端口号          if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {                 printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);                 exit(0);         }          if (listen(listenfd, 10) < 0) {                 printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);                 exit(0);         }          printf("=========waiting for client's request.....=====\n");               //    if ( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {       //          printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);                 //continue;       //  }          while(1) {         	// ccept(listenfd, (struct sockaddr*)&clientaddr, &addrlen);  //clientaddr  ip地址, &addrlen 端口号 		connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); 		if(connfd<0){ 			printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno); 			return -1; 		} 		 		if(connfd>0){ 			while(1){ 			n = recv(connfd, buff, MAXLINE, 0); //断开连接后则不再阻塞 			if(n==0){ 				printf("connect %d close\n",connfd); 				printf("=========waiting for client's request.....=====\n"); 				close(connfd); 				 				break; 			}                 	buff[n] = '\0';                 	printf("recv msg from client: %s\n", buff);                 	                 	ret = strcmp(buff, "exit\n"); //相等返回0,客户端发送exit\n则断开连接                 	if (ret == 0){                 		close(connfd);                 		break;                 	} 			 			}                        	   		}         }         close(listenfd);         return 0; }  

实现多线程

TCP服务器:该代码实现了一个基本的TCP服务器,能够监听指定端口上的连接请求,并接受来自客户端的数据。

多线程处理:服务器使用多线程来同时处理多个客户端连接。每个新连接都会触发一个新线程的创建,该线程负责与该客户端的通信。主线程(main函数中的循环)继续监听新的连接请求,并为每个新请求创建新的线程。这个过程会一直持续下去,直到程序被外部方式(如用户中断)终止。

数据接收与响应:服务器能够接收来自客户端的数据,并打印到标准输出。如果接收到特定的字符串(exit\n),则关闭与该客户端的连接。

错误处理:代码中包含了对套接字操作(如创建、绑定、监听、接受连接)的错误处理,如果操作失败,则会打印错误信息并退出程序(或继续监听其他连接)

#include<stdio.h>   #include<stdlib.h>   #include<string.h>   #include<errno.h>   #include<sys/types.h>   #include<sys/socket.h>   #include<netinet/in.h>   #include<unistd.h>   #include <arpa/inet.h>   #include <pthread.h>      #define MAXLINE 4096      void* handle_client(void* arg) {       int connfd = *(int*)arg;       free(arg);  // 释放传递的指针       char buff[MAXLINE];       int n;          while (1) {           n = recv(connfd, buff, MAXLINE, 0);           if (n == 0) {               printf("Client %d disconnected\n", connfd);               close(connfd);               break;           }           buff[n] = '\0';           printf("Received from client: %s\n", buff);              if (strcmp(buff, "exit\n") == 0) {               close(connfd);               break;           }              // 在这里可以添加对客户端消息的处理逻辑       }          return NULL;   }      int main(int argc, char** argv)   {       int listenfd, connfd;       struct sockaddr_in serveraddr;       pthread_t tid;       int *new_sock;       char buff[MAXLINE];     int n, ret;       if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol      	printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);         exit(0);      }       memset(&serveraddr, 0, sizeof(serveraddr));      serveraddr.sin_family = AF_INET; //ipv4网络类型      serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128"));  // INADDR_ANY: set addr 0.0.0.0, means all addr      serveraddr.sin_port = htons(1234);  // 绑定端口号       if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {          printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);          exit(0);      }       if (listen(listenfd, 10) < 0) {          printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);          exit(0);      }         printf("=========Waiting for client's request.....=====\n");          while (1) {           connfd = accept(listenfd, NULL, NULL);           if (connfd < 0) {               printf("Accept error: %s (errno: %d)\n", strerror(errno), errno);               continue;           }              // 为新连接创建新线程           new_sock = malloc(sizeof(int));           *new_sock = connfd;           if (pthread_create(&tid, NULL, handle_client, (void*)new_sock) != 0) {               perror("Failed to create thread");               close(connfd);               free(new_sock);               continue;           }              // 可以在这里继续处理其他连接,或者什么都不做(由主线程循环负责)       }          close(listenfd);       return 0;   } 

mysql部分

mysql_real_connect

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,                             const char *passwd, const char *db, unsigned int port,                             const char *unix_socket, unsigned long clientflag); 
  • mysql: 指向 MYSQL 结构的指针,这个结构由 mysql_init 函数初始化。这个结构包含了连接所需的所有信息。
  • host: MySQL 服务器的地址,可以是 IP 地址或者域名。如果 host 是 NULL 或 “localhost”,客户端将尝试通过 UNIX socket 连接。
  • user: 连接 MySQL 服务器所使用的用户名。
  • passwd: 用户的密码。如果密码是 NULL,客户端将尝试使用空密码进行连接。
  • db: 尝试连接后立即使用的默认数据库名。如果不需要默认数据库,可以将其设置为 NULL。
  • port: MySQL 服务器的端口号。如果设置为 0,客户端将使用默认的 MySQL 端口(通常是 3306)。
  • unix_socket: UNIX socket 文件的路径(如果服务器运行在 UNIX 上)。如果 host 参数是 NULL 或 “localhost”,并且 unix_socket 不是 - NULL,客户端将尝试通过 UNIX socket 而不是 TCP/IP 连接到服务器。
  • clientflag: 一个位掩码,用于修改客户端的默认行为。这些标志可以组合使用,以提供对连接行为的更细粒度控制。例如MYSQL_CLIENT_COMPRESS 可以用来请求使用压缩协议。
  • 返回值: 成功,mysql_real_connect 返回一个指向 MYSQL 结构的指针,该结构与传递给函数的指针相同;失败,返回 NULL。你可以通过调用 mysql_error 函数来获取关于连接失败的错误消息。

在使用完 MYSQL 结构后,应使用 mysql_close 函数来关闭连接并释放相关资源。
mysql_real_connect 是线程安全的,但 MYSQL 结构本身不是线程安全的。如果你在多线程环境中工作,每个线程都应该有自己的 MYSQL 结构实例。

mysql_error (指定要使用的 MySQL 连接标识符) 

返回值
成功时,如果上一个操作没有错误,则返回空字符串(“”)。
失败时,返回描述上一个 MySQL 操作的错误的字符串。

#include <mysql.h>   #include <stdio.h>   #include <stdlib.h>      int main() {       MYSQL *conn;  //MYSQL 是一个结构体类型,它包含了与 MySQL 数据库服务器建立连接所需的所有信息     MYSQL_RES *res;  //指向MYSQL_RES的指针,存储结果     MYSQL_ROW row;  //一个字符指针数组(char **)的别名,它用于表示结果集中的一行数据        // 初始化 MySQL 连接       conn = mysql_init(NULL);          // 连接到 MySQL 数据库       if (!mysql_real_connect(conn, "localhost", "your_username", "your_password", "testdb", 0, NULL, 0)) {      		 //fprintf 函数是 C 语言标准库中的一个非常有用的函数,它用于向文件或标准输         fprintf(stderr, "%s\n", mysql_error(conn));  出(如屏幕)写入格式化的数据。         exit(1);       }          // 插入数据       if (mysql_query(conn, "INSERT INTO users (name) VALUES ('John Doe')")) {           fprintf(stderr, "%s\n", mysql_error(conn));           mysql_close(conn);           exit(1);       }          // 查询数据       if (mysql_query(conn, "SELECT * FROM users")) {           fprintf(stderr, "%s\n", mysql_error(conn));           mysql_close(conn);           exit(1);       }          res = mysql_use_result(conn);       while ((row = mysql_fetch_row(res)) != NULL) {           printf("%s\n", row[1]); // 假设 name 是第二列(索引从 0 开始)       }       mysql_free_result(res);          // 更新数据       if (mysql_query(conn, "UPDATE users SET name = 'Jane Doe' WHERE id = LAST_INSERT_ID()")) {           fprintf(stderr, "%s\n", mysql_error(conn));           mysql_close(conn);           exit(1);       }          // 再次查询以验证更新       if (mysql_query(conn, "SELECT * FROM users")) {           fprintf(stderr, "%s\n", mysql_error(conn));           mysql_close(conn);           exit(1);       }          res = mysql_use_result(conn);       while ((row = mysql_fetch_row(res)) != NULL) {           printf("%s\n", row[1]);       }       mysql_free_result(res);          // 删除数据       if (mysql_query(conn, "DELETE FROM users WHERE id = LAST_INSERT_ID()")) {           fprintf(stderr, "%s\n", mysql_error(conn));           mysql_close(conn);           exit(1);       }          // 关闭数据库连接       mysql_close(conn);          return 0;   } 

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!