socket 多进程/多线程模型实现

前文我们实现了一个 socket 最小的实现,它只允许一台终端连接到服务器进行数据通信,但这样的程序对我们来说没有什么意义,所以我们一定要实现多个客户端与一个服务端通信交互数据,这样才能真正派上用场,所以本文主要介绍了两种实现多客户端连接的方案,一种是多进程,一种是多线程,两种性能相差无几,但明显多线程在资源方面明显要比多进程消耗要少的多。


公共头文件

/* wrap.h */
#ifndef __WRAP_H__
#define __WRAP_H__

void perr_exit(const char* s);
int Accept(int fd, struct sockaddr* sa, socklen_t* salenptr);
void Bind(int fd, const struct sockaddr* sa, socklen_t salen);
void Connect(int fd, const struct sockaddr* sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void* ptr, size_t nbytes);
ssize_t Write(int fd, const void* ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void* vptr, size_t n);
ssize_t Writen(int fd, const void* vptr, size_t n);
static ssize_t my_read(int fd, char* ptr);
ssize_t Readline(int fd, void* vptr, size_t maxlen);

#endif
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
#include "wrap.h"

void perr_exit(const char* s)
{
	perror(s);
	exit(1);
}

int Accept(int fd, struct sockaddr* sa, socklen_t* salenptr)
{
	int n;

again:
	if ( (n = accept(fd, sa, salenptr)) < 0 )
	{
		if ((errno == ECONNABORTED) || (errno == EINTR))
		{
			goto again;
		}
		else
		{
			perr_exit("accept error");
		}
	}
	return n;
}

void Bind(int fd, const struct sockaddr* sa, socklen_t salen)
{
	if ( bind(fd, sa, salen) < 0 )
	{
		perr_exit("bind error");
	}
}

void Connect(int fd, const struct sockaddr* sa, socklen_t salen)
{
	if ( connect(fd, sa, salen) < 0 )
	{
		perr_exit("connect error");
	}
}

void Listen(int fd, int backlog)
{
	if ( listen(fd, backlog) < 0 )
	{
		perr_exit("listen error");
	}
}

int Socket(int family, int type, int protocol)
{
	int n = socket(family, type, protocol);
	if ( n < 0 )
	{
		perr_exit("socket error");
	}
	return n;
}

ssize_t Read(int fd, void* ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = read(fd, ptr, nbytes)) == -1)
	{
		if (errno == EINTR)
		{
			goto again;
		}
		else
		{
			return -1;
		}
	}
	return n;
}

ssize_t Write(int fd, const void* ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = write(fd, ptr, nbytes)) == -1)
	{
		if (errno == EINTR)
		{
			goto again;
		}
		else
		{
			return -1;
		}
	}
	return n;
}

void Close(int fd)
{
	if (close(fd) == -1)
	{
		perr_exit("close error");
	}
}

ssize_t Readn(int fd, void* vptr, size_t n)
{
	size_t nleft;
	ssize_t nread;
	char* ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0)
	{
		if ( (nread = read(fd, ptr, nleft)) < 0 )
		{
			if (errno == EINTR)
			{
				nread = 0;
			}
			else
			{
				return -1;
			}
		}
		else if (nread == 0)
		{
			break;
		}
		
		nleft -= nread;
		ptr += nread;
	}
}

ssize_t Writen(int fd, const void* vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char* ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0)
	{
		if ( (nwritten = write(fd, ptr, nleft)) <= 0)
		{
			if (nwritten < 0 && errno == EINTR)
			{
				nwritten = 0;
			}
			else
			{
				return -1;
			}
		}

		nleft -= nwritten;
		ptr += nwritten;
	}

	return n;
}

static ssize_t my_read(int fd, char* ptr)
{
	static int read_cnt;
	static char* read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0)
	{
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0 )
		{
			if (errno == EINTR)
			{
				goto again;
			}
			return -1;
		}
		else if (read_cnt == 0)
		{
			return 0;
		}

		read_ptr = read_buf;
	}

	read_cnt--;
	*ptr = *read_ptr++;
	return 1;
}

ssize_t Readline(int fd, void* vptr, size_t maxlen)
{
	ssize_t n, rc;
	char c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++)
	{
		if ( (rc = my_read(fd, &c)) == 1)
		{
			*ptr++ = c;
			if (c == '\n')
			{
				break;
			}
		}
		else if (rc == 0)
		{
			*ptr = 0;
			return n - 1;
		}
		else
		{
			return -1;
		}

		*ptr = 0;
		return n;
	}
}

客户端代码(多进程多线程共用)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "wrap.h"

#define MAXLINE 80
#define SRV_PORT 8000

int main(int argc, char* argv[])
{
	struct sockaddr_in srv_addr;
	int sock, conn;

	sock = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&srv_addr, sizeof(srv_addr));
	srv_addr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr);
	srv_addr.sin_port = htons(SRV_PORT);

	conn = Connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr));

	int len;
	char buf[MAXLINE];
	while (fgets(buf, sizeof(buf), stdin))
	{
		Write(sock, buf, strlen(buf));
		len = Read(sock, buf, sizeof(buf));
		if (len == 0)
		{
			printf("server is closed...\n");
			break;
		}
		else
		{
			Write(STDOUT_FILENO, buf, len);
		}
	}

	close(sock);
	return 0;
}

多进程服务端代码

#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"

#define MAXLINE 80
#define SRV_PORT 8000

int main(int argc, char* argv[])
{
	socklen_t cnt_len;
	struct sockaddr_in srv_addr, cnt_addr;

	int sock, conn;
	int n, i;
	pid_t pid;

	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	
	// 创建socket
	sock = Socket(AF_INET, SOCK_STREAM, 0);
	bzero(&srv_addr, sizeof(srv_addr));

	srv_addr.sin_family = AF_INET;
	srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	srv_addr.sin_port = htons(SRV_PORT);

	// 绑定ip和端口
	Bind(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
	// 开始监听
	Listen(sock, 20);

	printf("Accepting connections ...\n");
	while (1)
	{
		// 等待接收客户端连接
		cnt_len = sizeof(cnt_addr);
		conn = Accept(sock, (struct sockaddr*)&cnt_addr, &cnt_len);	
		printf("received from %s at PORT %d\n", 
				inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)),
				ntohs(cnt_addr.sin_port));

		// 有新客户端连接创建子进程
		pid = fork();
		if (pid == 0)
		{
			// 关闭从父进程继承下来的 socket 文件描述符
			Close(sock);
			while (1)
			{
				// 从客户端连接后产生的新文件描述符中读取数据
				n = Read(conn, buf, MAXLINE);
				// 如果读取返回值为0证明客户端退出了
				if (n == 0)
				{
					printf("client exit...\n");
					break;
				}
				// 打印连接的客户端ip和端口号
				printf("%s:%d", 
						inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)),
						ntohs(cnt_addr.sin_port));

				// 将读取到的buf数据打印到屏幕上
				Write(STDOUT_FILENO, buf, n);

				// 将数据中所有字符转换为大写
				for(i = 0; i < n; i++)
				{
					buf[i] = toupper(buf[i]);
				}
				// 重新发送给客户端
				Write(conn, buf, n);
			}
			// 关闭与客户端的连接
			Close(conn);
			return 0;
		}
		// 如果是父进程那么关闭新来的连接文件描述符并且后面什么都不做
		else if (pid > 0)
		{
			Close(conn);
		}
		else
		{
			perr_exit("fork");
		}
	}

	// 最后关闭socket文件描述符
	Close(sock);
	return 0;
}

编译多进程程序运行测试

编译客户端:gcc client.c wrap.c -o client

编译服务端:gcc server_fork.c wrap.c -o server_fork

运行效果:

2015-07-11 10:21:15

多线程服务端代码

#include <string.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <pthread.h>

#define MAXLINE 80
#define SRV_PORT 8000

struct tag_thread
{
	int conn;
	struct sockaddr_in sockaddr;
};

void* recv_thread(void* arg)
{
	// 将自身线程设置为游离态,无需再次回收
	pthread_detach(pthread_self());
	// 将参数转换为可识别的结构体
	struct tag_thread* new_conn = (struct tag_thread*)arg;

	char buf[1024];
	char str[INET_ADDRSTRLEN];
	int len;
	int i;
	
	while (1)
	{
		// 从传递进来的文件描述符中读取数据
		len = Read(new_conn->conn, buf, sizeof(buf));
		if (0 == len)
		{
			pthread_exit(NULL);
		}
		printf("recv client data %s:%d\n", 
			inet_ntop(AF_INET, &new_conn->sockaddr.sin_addr, str, sizeof(str)),
			ntohs(new_conn->sockaddr.sin_port));
		// 打印读取到的数据到屏幕
		Write(STDOUT_FILENO, buf, len);
		// 将读取到的数据转换为大写
		for (i = 0; i < strlen(buf); i++)
		{
			buf[i] = toupper(buf[i]);
		}
		// 重新发回给客户端
		Write(new_conn->conn, buf, len);
	}

	return (void*)0;
}

int main(int argc, char* argv[])
{
	socklen_t cnt_len;
	struct sockaddr_in srv_addr, cnt_addr;

	int sock, conn;
	int n, i;
	pid_t pid;
	pthread_t tid;

	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];

	sock = Socket(AF_INET, SOCK_STREAM, 0);
	bzero(&srv_addr, sizeof(srv_addr));

	srv_addr.sin_family = AF_INET;
	srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	srv_addr.sin_port = htons(SRV_PORT);

	Bind(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
	Listen(sock, 20);

	printf("Accepting connections ...\n");
	while (1)
	{
		struct tag_thread new_conn;
		cnt_len = sizeof(cnt_addr);
		// 阻塞等待新的客户端连接
		conn = Accept(sock, (struct sockaddr*)&cnt_addr, &cnt_len);
		// 打印新来的客户端ip和端口
		printf("while received from %s at PORT %d\n", 
				inet_ntop(AF_INET, &cnt_addr.sin_addr, str, sizeof(str)),
				ntohs(cnt_addr.sin_port));

		// 设置要传递给线程函数的结构体内容
		new_conn.conn = conn;				// 新连接的文件描述符
		new_conn.sockaddr = cnt_addr;		// 新连接的属性结构体

		// 创建线程
		pthread_create(&tid, NULL, recv_thread, (void*)&new_conn);
	}

	Close(sock);
	return 0;
}

编译多线程程序运行测试

编译客户端:gcc client.c wrap.c -o client

编译服务端:gcc server_thread.c wrap.c -o server_thread -l pthread

运行效果:

2015-07-11 10:42:55

发表评论