Linux TCP多进程并发服务器实现详解

网络通信的过程:
服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。客户端依次调用socket()、connect()之后就向服务器发送了一个连接请求。服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了

1.socket函数:创建套接字

函数原型为:
int socket(int domain, int type, int protocol)1.第一个参数为协议族,常用AF_INET,internet协议族
2.套接字类型,常用的有三种:
			SOCK_STREAM	  TCP套接字,按顺序发送(四层结构)
			SOCK_DGRAM   UDP套接字,按顺序发送
			SOCK_RAW  原始套接字,发送的时候穿透了传输层
3.成功返回一个文件描述符,失败返回-1

2.bind函数:把地址和端口号的组合赋给socket

函数原型为:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)1.第一个参数为 函数返回的文件描述符
2.第二个参数用来保存ip地址和端口号
	IP地址(IPV4)占有4个字节,端口号占有2个字节,因此定义了一个结构体struct sockaddr来存储,	这个
	结构体含有两个成员,第一个成员占据两个字节的大小,存放协议族,一般使用Internet,即AF_INET。第二
	个成员也是一个结构体,占据14个字节的大小,存放ip地址和端口号,但是初始化不方便,因此采用另一种结
	构体struct sockaddr_in.
struct sockaddr_in
  {           
       u_short sin_family;      // 地址族, AF_INET,2 bytes
       u_short sin_port;      // 端口,2 bytes  0---65536
       struct in_addr sin_addr;  // IPV4地址,4 bytes 	
       char sin_zero[8];        // 8 bytes unused,作为填充
  }; 

	该结构体含有四个成员,分别对应以上的三个元素和另外的8个字节的补充单元。初始化ip地址时用inet_addr
	函数转换成无符号长整型。但是传参数的时候需要强制类型转换成struct sockaddr*类型的
3.第二个参数占用空间的大小,strlen(struct sockaddr)
4.函数成功执行返回0,失败返回-1
	

3.listen函数:服务器端的监听函数,监听socket,如果客户端调用connect发出链接请求,服务器就会接收到

函数原型:
int listen(int sockfd, int backlog)1.socket函数返回的文件描述符
2.设置客户端请求的队列长度,大多数系统允许数目为20
3.函数成功执行返回0,失败返回-1

4.accept函数:接收客户端的请求

函数原型为:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)1.第一个参数为socket函数返回的文件描述符
2.第二个才参数包含客户端的ip地址和端口号,不需要可以设置为NULL
3.描述第二个参数的大小,不需要可以设置为NULL

5.区分监听套接字和链接套接字
监听套接字只是负责监听是否有请求,当客户端发来请求以后,调用accpet函数,并返回一个链接套接字,此时与客户端的通信完全是由这个链接套接字来完成,而监听套接字依旧在监听。

6.connect函数:客户端通过调用connect函数向服务器端请求连接

函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
与bind函数的参数完全一致,一个用在服务器端一个用在客户端
1.第一个参数:客户端的socket描述符
2.第二个参数:服务器端的socket地址,内涵ip和端口号
3.第三个长度:socket地址的长度

7.IP地址的转换

主机字节序到网络字节序:
	1.inet_pton(AF_INET,"ip地址",(void *)&var.sin_addr);//成功返回1,适用于ipv4和ipv6
	2.var.sin_addr.s_addr = inet_addr("ip地址");
	3.var.sin_addr.s_addr = htonl(INADDY_ANY);//可以绑定任意的ip,只要端口号正确,套接字类型正确
	就都可以处理
网络字节序到主机字节序:
	1.inet_ntop(AF_INET,(void *)&var2.sin_addr,buf,sizeof(var2));//失败返回NULL,使用ipv4和ipv6
	

8.端口号的转换:

主机字节序到网络字节序:
	htons(端口号)//自行分配的端口号最好在5000以后
网络字节序到主机字节序:
	ntohs(端口号);
还有两个函数htonl和ntohl用于32位数的转换。

9.程序实现:C\S模式下的互发消息

//server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERV_PORT 5001
#define BACKLOG 5

void exitfun(int sig);
void *sendfun(void *arg);

int main(int argc, char *argv[])
{
	int fd;
	int rw_fd;
	pthread_t tid;
	int ret;
	pid_t pid;
	socklen_t len = sizeof(struct sockaddr);
	struct sockaddr_in sin = {0};
	struct sockaddr_in cin = {0};
	char buf[32] = {0};

	//创建socket
	if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror("socket");
		exit(-1);
	}

	//初始化并绑定本机的ip和端口
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);
	sin.sin_addr.s_addr = htonl(INADDR_ANY);

	if((bind(fd, (struct sockaddr *)&sin, sizeof(sin))) < 0)
	{
		perror("bind");
		exit(-1);
	}

	//监听socket
	if(listen(fd, BACKLOG) < 0)
	{
		perror("listen");
		exit(-1);
	}
	signal(10,exitfun);			//接受子进程或者线程发来的信号,当通信结束后结束进程并回收资源

	while(1)		//循环调用,每当有客户端发送链接请求就产生一个子进程去与之通信,主进程继续监听
	{

		if((rw_fd = accept(fd, (struct sockaddr *)&cin, &len)) < 0)		//接收客户端请求
		{
			perror("accept");
			exit(-1);
		}

		if((inet_ntop(AF_INET, (void *)&cin.sin_addr, buf, sizeof(cin))) == NULL)		//转化客户端的ip地址
		{
			perror("inet_ntop");
			exit(-1);
		}

		printf("Cilent (%s:%d) is connected!\n", buf, ntohs(cin.sin_port));		//打印客户端ip和端口号

		pid = fork();

		if(pid == 0)
		{
			close(fd);
			if((ret = pthread_create(&tid, NULL, sendfun, (void *)&rw_fd)) == -1)	//子线程负责发送消息
			{
				perror("pthread_create");
				exit(-1);
			}

			while(1)			//主线程负责接收消息
			{
				memset(buf, 0, 32);
				read(rw_fd, buf, 32);
				if(strcmp(buf, "quit") == 0)
				{
					close(rw_fd);
					kill(getppid(), 10);
					exit(-1);			//线程推出,进程也就退出了
				}
				printf("receive from cilent = %s\n",buf);
			}
		}

		if(pid > 0)
		{
			close(rw_fd);			//只有父子进程读关闭文件,这个文件才会真正被关闭,一方关闭不影响另一方使用
		}
	}
	close(fd);
	return 0;
}

void exitfun(int sig)
{
	printf("cilent exit\n");
	wait(NULL);
}

void *sendfun(void *arg)
{
	char buf[32] = {0};
	int rw_fd;

	rw_fd = *((int *)arg);
	while(1)
	{
		scanf("%s",buf);
		write(rw_fd, buf, strlen(buf));
		if(strcmp(buf, "quit") == 0)
		{
			close(rw_fd);
			kill(getppid(),10);			//客户端发送quit,断开链接,发送信号给父进程,回收资源
			exit(-1);					//子线程中使用exit进程也会结束
		}
	}
}

//cilent
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERV_PORT 5001
#define BACKLOG 5

void *readfun(void *arg);

int main(int argc, char *argv[])
{
	int fd;
	pthread_t tid;
	int ret;
	struct sockaddr_in sin = {0};
	char buf[32] = {0};

	//创建socket
	if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror("socket");
		exit(-1);
	}

	//初始化并绑定本机的ip和端口
	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_PORT);
	sin.sin_addr.s_addr = inet_addr("192.168.30.170");

	if((connect(fd, (struct sockaddr *)&sin, sizeof(sin))) < 0)
	{
		perror("bind");
		exit(-1);
	}

	if((ret = pthread_create(&tid, NULL, readfun, (void *)&fd)) == -1)	//子线程负责发送消息
	{
		perror("pthread_create");
		exit(-1);
	}

	while(1)			//主线程负责发送消息
	{
		scanf("%s", buf);
		write(fd, buf, strlen(buf));
		if(strcmp(buf, "quit") == 0)
		{
			break;
		}
		memset(buf, 0, 32);	
	}

	return 0;
}

void *readfun(void *arg)		//子线程负责接收消息
{
	char buf[32] = {0};
	int fd;

	fd = *((int *)arg);
	while(1)
	{
		read(fd, buf, 32);
		if(strcmp(buf, "quit") == 0)
		{
			exit(-1);
		}
		printf("receive from server = %s\n", buf);
		memset(buf, 0, 32);
	}
}

10.关于文件的关闭
父子进程中如果有共享的文件描述符,如果用不到就在开头直接关闭(并不会影响另一个的使用),并且需要用的文件描述符也要在用完后关闭,相当于每个文件必须关闭两次

11.常见问题
1.打印客户端地址为0:accept函数的最后一个参数在定义时需要设置初值,大小为16,或者sizeof(struct sockaddr_in);
https://www.cnblogs.com/jiangzhaowei/p/8261174.html

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值