Linux网络并发编程
Linux网络并发编程涵盖多个方面,主要包括多线程编程、多进程编程(进程间通信)、非阻塞I/O、异步I/O、事件驱动编程、并发数据结构等内容。Linux网络并发编程各个部分的具体内容如下:
- 多线程编程:使用多线程来处理并发任务是常见的做法。在Linux中,可以使用pthread库来创建和管理线程。
- 进程间通信(IPC):在多线程或多进程环境中,进程或线程之间需要交换数据或协调任务。Linux提供了多种IPC机制,如管道、消息队列、信号量、共享内存等。
- 非阻塞I/O:传统的I/O操作是阻塞的,即当数据未准备好时,线程或进程会被阻塞。为了避免阻塞,可以使用非阻塞I/O。Linux提供了O_NONBLOCK标志来实现非阻塞I/O。
- 异步I/O(AIO):异步I/O允许发起I/O操作后立即返回,数据准备就绪时再通知应用程序。Linux提供了AIO接口来支持异步I/O。
- 事件驱动编程:事件驱动编程模型允许程序响应各种事件,例如网络连接、数据到达等。Linux的事件驱动编程可以通过使用epoll机制实现。
- 并发数据结构:在并发环境中,需要使用特殊的数据结构来保证数据的一致性和线程安全。例如,队列、栈、哈希表等都需要进行适当的同步和互斥。
对于Linux网络程序多线程编程,程序的主要实现方式为:主程序创建一个新的套接字描述符,绑定相关IP地址、端口等,监听该套接字;当从客户端收到连接时,就利用pthread库函数创建一个子线程专门管理该连接的消息接收等,当收到预定义的指令时结束该线程。运行流程图如下:
pthread库的使用
在Linux操作系统下,多线程编程遵循POSIX线程接口,简称pthread。相关接口在pthread库中提供,该库为开发人员提供了丰富的函数和工具,用于创建、管理和同步线程。以下是一些常用的pthread函数及其功能描述:
- pthread_create(): 用于创建一个新的线程。它接受一个线程属性参数,以及一个指向线程函数的指针,该函数将在新线程中执行。
- pthread_join(): 允许一个线程等待另一个线程的终止。它可以获取已终止线程的退出状态。
- pthread_mutex_init(): 初始化一个互斥锁,用于保护共享数据免受并发访问的影响。
- pthread_mutex_lock(): 获取互斥锁,以防止其他线程同时访问受保护的代码区域。
- pthread_mutex_unlock(): 释放互斥锁,允许其他线程获取锁并访问受保护的代码区域。
- pthread_cond_init(): 初始化一个条件变量,用于线程之间的协调和同步。
- pthread_cond_wait(): 使线程等待某个条件成立,释放锁并进入等待状态,直到其他线程发出通知。
- pthread_cond_signal(): 通知一个等待在条件变量上的线程,使其继续执行。
- pthread_cond_broadcast(): 通知所有等待在条件变量上的线程,使其继续执行。
- pthread_detach(): 将指定线程分离,使其在终止时自动释放资源。
这些函数只是pthread库中的一部分,还有其他许多函数可用于实现更复杂的并发操作和同步机制。通过合理使用这些函数,开发人员可以构建高效、可靠的并发应用程序。
pthread_create()函数原型如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void
*arg);
- 第一个参数thread是一个pthread_t类型的指针,它用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取自己的线程ID(pthread_t类型)
- 第二个参数是线程的属性attr,其类型是 pthread_attr_t 结构体类型。
- 第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(回调函数,返回值是void *类型,形参是void *)
- 第四个参数arg是传给所调用的函数的参数,如果有多个参数要传递的话,就需要将这多个参数封装到一个结构体中,再传入函数中。
pthread_attr_t结构体定义如下:
typedef struct
{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void *stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
pthread库的编译
一般情况下,在链接一个(文件名为libxxx.so或libxxx.a等的)库时,会使用-lxxx的方式;在Linux中要用到多线程时需要链接pthread库,按照惯例应该使用-lpthread的方式来进行链接。然而很多开源代码都是使用了-pthread参数而非使用-lpthread,主要原因如下:
- 为了可移植性:在Linux中,pthread是作为一个单独的库存在的(libpthread.so),但是在其他Unix变种中却不一定,比如在FreeBSD中是没有单独的pthread库的,因此在FreeBSD中不能使用-lpthread来链接pthread,而使用-pthread则不会存在这个问题,因为FreeBSD的编译器能正确将-pthread展开为该系统下的依赖参数。同样道理,其他不同的变种也会有这样那样的区别,如果使用-lpthread,则可能在移植到其他Unix变种中时会出现问题,为了保持较高的可移植性,我们最好还是使用-pthread(尽管这种做法未被接纳成为C标准,但已基本是事实标准)。
- 添加额外的标志:在多数系统中,-pthread会被展开为“-D_REENTRANT -lpthread”,即是除了链接pthread库外,还先定义了宏_REENTRANT。定义这个宏的目的,是为了打开系统头文件中的各种多线程支持分支。比如,我们常常使用的错误码标志errno,如果没有定义_REENTRANT,则实现为一个全局变量;若是定义了_REENTRANT,则会实现为每线程独有,从而避免线程竞争错误。
下面对服务端server.c的代码进行详细分析,客户端client.c的代码实现与服务端较为相似,具体源码均附在文章末尾,故不再赘述。
socket函数创建套接字
使用socket()函数创建一个新的套接字,返回套接字描述符。第一个参数指明使用的协议栈,AF_INET(或PF_INET)指明使用TCP/IP协议栈。第二个参数SOCK_STREAM指明使用流服务,第三个参数取0,默认值。
/*(1) 创建套接字*/
if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
perror("socket error.\n");
exit(1);
}//if
初始化地址结构
下面的代码用于初始化服务端的地址结构,首先使用bzero()将地址内容置零,AF_INET表示TCP/IP地址,INADDR_ANY表示自动设置本机IP,PORT宏设置本机端口。
/*(2) 初始化地址结构*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
bind函数绑定套接字和端口
使用bind()函数为套接字指明一个本地端点地址TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟知的端口号,然后等待连接。listenfd为指定的套接字描述符;servaddr为设置好的地址结构,包括IP地址和端口号;第三个参数sizeof(servaddr)为地址长度。
/*(3) 绑定套接字和端口*/
if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
perror("bind error.\n");
exit(1);
}//if
listen函数监听端口
使用listen()函数将一个套接字置为被动模式,并准备接收传入连接。用于服务器,指明某个套接字连接是被动的。第一个参数listenfd指明用于创建连接的套接字描述符;第二个参数LISTENQ表示该套接字使用的队列长度,即指定在请求队列中允许的最大请求数。
/*(4) 监听*/
if(listen(listenfd , LISTENQ) < 0)
{
perror("listen error.\n");
exit(1);
}//if
accept函数接受请求
使用accept()函数获取传入连接请求,返回新的连接的套接字描述符。该函数将为每个新的连接请求创建一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字,使用完毕,服务器将关闭这个套接字。第一个参数listenfd指明正在监听的套接字描述符;第二个参数cliaddr用于记录请求连接的主机地址;第三个参数client用与指明地址长度。
/*(5) 接受客户请求*/
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
{
perror("accept error.\n");
exit(1);
}//if
phtread_create创建子线程
使用pthread_create()创建一个新的线程。它接受一个线程属性参数,以及一个指向线程函数的指针,该函数将在新线程中执行。第一个参数recv_tid用于获取线程的thread ID;第二个参数默认为NULL;第三个参数recv_message是一个函数指针,用于指明线程需要执行的任务;第四个参数connfd用于指明向线程传递的参数,即新建立的连接的套接字描述符。
/*(6) 创建子线程处理该客户链接接收消息*/
if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1)
{
perror("pthread create error.\n");
exit(1);
}//if
recv_message接收消息函数
使用recv_message()函数处理接收客户端消息的过程。该函数在服务器与客户端建立连接后,在服务器的子线程中执行。参数fd指明与客户端建立连接的套接字描述符。当该线程通过从recv()调用中返回后,若从客户端接收的消息不是'byebye.'字符串,则在服务器端打印来自客户端的消息并注明来自客户端;若从客户端接收的消息是'byebye.'字符串,则在服务器端打印客户端已关闭,并关闭这个连接的套接字描述符,并结束该线程。
/*处理接收客户端消息函数*/
void *recv_message(void *fd)
{
int sockfd = *(int *)fd;
while(1)
{
char buf[MAX_LINE];
memset(buf , 0 , MAX_LINE);
int n;
if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
{
perror("recv error.\n");
exit(1);
}//if
buf[n] = '\0';
//若收到的是'byebye.'字符串,则代表退出通信
if(strcmp(buf , "byebye.") == 0)
{
printf("Client closed.\n");
close(sockfd);
exit(1);
}//if
printf("\nClient: %s\n", buf);
}//while
}
发送消息
服务端在main()函数中使用fgets()调用获取用户的输入。若服务端输入的字符串不为'exit',则使用send()调用向客户端发送输入的字符串;若服务端输入的字符串为'exit',则在服务端打印服务端已关闭,向客户端发送'byebye.'字符串并关闭该连接的套接字描述符,最后结束该线程。
/*处理服务器发送消息*/
char msg[MAX_LINE];
memset(msg , 0 , MAX_LINE);
while(fgets(msg , MAX_LINE , stdin) != NULL)
{
if(strcmp(msg , "exit\n") == 0)
{
printf("Server closed.\n");
memset(msg , 0 , MAX_LINE);
strcpy(msg , "byebye.");
send(connfd , msg , strlen(msg) , 0);
close(connfd);
exit(0);
}//if
if(send(connfd , msg , strlen(msg) , 0) == -1)
{
perror("send error.\n");
exit(1);
}//if
}//while
代码实现
server端
/*
* 服务器端代码实现
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>
const int MAX_LINE = 2048;
const int PORT = 6001;
const int BACKLOG = 10;
const int LISTENQ = 6666;
const int MAX_CONNECT = 20;
/*处理接收客户端消息函数*/
void *recv_message(void *fd)
{
int sockfd = *(int *)fd;
while(1)
{
char buf[MAX_LINE];
memset(buf , 0 , MAX_LINE);
int n;
if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
{
perror("recv error.\n");
exit(1);
}//if
buf[n] = '\0';
//若收到的是'byebye.'字符串,则代表退出通信
if(strcmp(buf , "byebye.") == 0)
{
printf("Client closed.\n");
close(sockfd);
exit(1);
}//if
printf("\nClient: %s\n", buf);
}//while
}
int main()
{
//声明套接字
int listenfd , connfd;
socklen_t clilen;
//声明线程ID
pthread_t recv_tid , send_tid;
//定义地址结构
struct sockaddr_in servaddr , cliaddr;
/*(1) 创建套接字*/
if((listenfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
perror("socket error.\n");
exit(1);
}//if
/*(2) 初始化地址结构*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
/*(3) 绑定套接字和端口*/
if(bind(listenfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
perror("bind error.\n");
exit(1);
}//if
/*(4) 监听*/
if(listen(listenfd , LISTENQ) < 0)
{
perror("listen error.\n");
exit(1);
}//if
/*(5) 接受客户请求*/
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd , (struct sockaddr *)&cliaddr , &clilen)) < 0)
{
perror("accept error.\n");
exit(1);
}//if
printf("server: got connection from %s\n", inet_ntoa(cliaddr.sin_addr));
/*(6) 创建子线程处理该客户链接接收消息*/
if(pthread_create(&recv_tid , NULL , recv_message, &connfd) == -1)
{
perror("pthread create error.\n");
exit(1);
}//if
/*处理服务器发送消息*/
char msg[MAX_LINE];
memset(msg , 0 , MAX_LINE);
while(fgets(msg , MAX_LINE , stdin) != NULL)
{
if(strcmp(msg , "exit\n") == 0)
{
printf("Server closed.\n");
memset(msg , 0 , MAX_LINE);
strcpy(msg , "byebye.");
send(connfd , msg , strlen(msg) , 0);
close(connfd);
exit(0);
}//if
if(send(connfd , msg , strlen(msg) , 0) == -1)
{
perror("send error.\n");
exit(1);
}//if
}//while
}
client端
/*
* 客户端代码
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>
const int MAX_LINE = 2048;
const int PORT = 6001;
const int BACKLOG = 10;
const int LISTENQ = 6666;
const int MAX_CONNECT = 20;
/*处理接收服务器消息函数*/
void *recv_message(void *fd)
{
int sockfd = *(int *)fd;
while(1)
{
char buf[MAX_LINE];
memset(buf , 0 , MAX_LINE);
int n;
if((n = recv(sockfd , buf , MAX_LINE , 0)) == -1)
{
perror("recv error.\n");
exit(1);
}//if
buf[n] = '\0';
//若收到的是exit字符,则代表退出通信
if(strcmp(buf , "byebye.") == 0)
{
printf("Server is closed.\n");
close(sockfd);
exit(0);
}//if
printf("\nServer: %s\n", buf);
}//while
}
int main(int argc , char **argv)
{
/*声明套接字和链接服务器地址*/
int sockfd;
pthread_t recv_tid , send_tid;
struct sockaddr_in servaddr;
/*判断是否为合法输入*/
if(argc != 2)
{
perror("usage:tcpcli <IPaddress>");
exit(1);
}//if
/*(1) 创建套接字*/
if((sockfd = socket(AF_INET , SOCK_STREAM , 0)) == -1)
{
perror("socket error");
exit(1);
}//if
/*(2) 设置链接服务器地址结构*/
bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if(inet_pton(AF_INET , argv[1] , &servaddr.sin_addr) < 0)
{
printf("inet_pton error for %s\n",argv[1]);
exit(1);
}//if
/*(3) 发送链接服务器请求*/
if( connect(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)) < 0)
{
perror("connect error");
exit(1);
}//if
/*创建子线程处理该客户链接接收消息*/
if(pthread_create(&recv_tid , NULL , recv_message, &sockfd) == -1)
{
perror("pthread create error.\n");
exit(1);
}//if
/*处理客户端发送消息*/
char msg[MAX_LINE];
memset(msg , 0 , MAX_LINE);
while(fgets(msg , MAX_LINE , stdin) != NULL)
{
if(strcmp(msg , "exit\n") == 0)
{
printf("Client closed.\n");
memset(msg , 0 , MAX_LINE);
strcpy(msg , "byebye.");
send(sockfd , msg , strlen(msg) , 0);
close(sockfd);
exit(0);
}//if
if(send(sockfd , msg , strlen(msg) , 0) == -1)
{
perror("send error.\n");
exit(1);
}//if
}//while
}
效果展示
首先分别启动服务端和客户端。建立连接后客户端向服务端发送'say hello',可以看到服务端正常打印出字符串'Client: say hello';随后服务端向客户端发送'say world',可以看到客户端正常打印字符串'Server: say world'。
在客户端(或服务端)输入'exit'字符串,将向服务端(或客户端)发送'byebye.'字符串并结束该连接。在下图中可以看到服务端接收到'byebye.'字符串后,在屏幕打印出'Client closed.'字符串。
致谢
本学期通过学习孟宁老师的网络程序设计实验的课程,了解了Javascript网络编程、Socket API、网络协议设计及RPC、Linux内核网络协议栈等在内的多个知识点,从协议内核到应用层网络编程,涉及知识面非常广阔,极大地拓展了本人的视野。
本人在对Socket API与多线程编程结合专题研究的过程中,进一步熟悉了Linux网络编程相关的知识点,加深了对这Socket API和pthread API的理解和应用。由于不论是Socket API还是pthread API对本人来说都是初次学习,项目中必然还存在很多问题。三人行必有我师焉,希望各位老师不吝赐教,学海无涯苦作舟!