Socket API多线程编程初探

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对本人来说都是初次学习,项目中必然还存在很多问题。三人行必有我师焉,希望各位老师不吝赐教,学海无涯苦作舟!

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值