Linux网络编程(四)——点对点聊天程序实现

目录

前言

这篇文章记录了实现一个简易点对点聊天程序的过程,软件开发比较好的地方就是能够看到许多日常事务的本质,但也导致出去玩的时候会没有惊奇感,不过知道更多的未知的才会更有意思吧!

函数介绍

setsockopt

服务器端尽可能使用SO_REUSEADDR
在绑定之前尽可能调用setsockopt来设置SO_REUSEADDR套接字选项。
使用SO_REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器。

// 功能:获取或者设置与某个套接字关联的选项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该将层的值指定为SOL_SOCKET。为了操作其他层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议号TCP.
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname,
               void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
               const void *optval, socklen_t optlen); 
参数1:sockfd 标识一个套接口的描述字
参数2:level 选项定义的层次; 支持SOL_SOCKET IPPROTO_TCP IPPROTO_IP IPPROTO_IPV6
参数3:optname 需设置的选项
参数4:optval 指针 指向存放选项待设置的新值的缓冲区
参数5:optlen optval缓冲区长度
返回值:若无错误发生,setsockopt()返回0.否则的话,返回SOCKET_ERROR(-1)。

当level为SOL_SOCKET时,比较常用的设置选项如下:
1. SO_RCVBUF和SO_SNDBUF:用于设置/读取发送缓冲区(SO_RCVBUF)和接收缓冲区大小(SO_SNDBUF),选项值类型:int,指定新的缓冲区大小,对setsockopt和getsockopt有效。
说明:设置缓冲区大小只能在TCP连接建立之前进行,TCP将接收缓冲区大小用于流量控制,UDP不提供流量控制,UDP没有实际的发送缓冲区,设置发送缓冲区的大小将改变能发送的最大UDP数据报的大小。使用如下:
int rcv_buf_size = 32*1024, snd_buf_size = 32*1024;  
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_buf_size, sizeof(int));  
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &SND_buf_size, sizeof(int));

2. SO_REUSEADDR:用于复用socket地址(端口号),选项值类型:int 0-不能复用 1-可以复用,默认值为0,对setsockopt和getsockopt有效。
复用地址一般用于下列情况:
	a.快速启动服务器:服务器在有客户端连接时终止,然后立即重启,由于TIME_WAIT状态,会导致原来的socket依然存在,bind操作将失败,如果指定SO_RUSEADDR选项可以避免这个问题。
	b.启动一个服务器程序的多个实例:当使用IP别名的时候,可以将若干个服务程序使用不同IP绑定到同一端口上。
	c.多个socket绑定同一端口:用于UDP程序在有多个网络接口时,为了区分数据报来自哪一个网络接口而使用。
举例:
    int on = 1;
    if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)                           
    {
        ERR_EXIT("setsockopt");
    }
3. SO_KEEPALIVE:用于TCPsocket检测/保持网络连接,这个选项被设置后,2小时以内双方没有数据交互,将发送一个探测数据段到对方,此时可能出现以下情况:
	a. 得到对方正确响应,继续等待下一次2小时超时;
	b. 收到RST数据段 返回错误ECONNRESET
	c. 对方无响应 多次发送探测数据段直到超时返回错误ETIMEOUT
	d. 选项值类型 int 0-不能发送 1-可以发送 默认值为0 对setsockopt和getsockopt有效

fork函数

一个进程,包括代码、数据和分配给进程的资源fork()函数通过系统调用创建一个原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。

代码示例

#include <stdio.h>                                                                                    
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#define ERR_EXIT(m) \
        do  \
        {   \
            perror(m);      \
            exit(EXIT_FAILURE); \
        }while(0)


void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int count = 0;
    pid_t pid;
    pid = fork();
    if(-1 == pid)
    {
        ERR_EXIT("fork");
    }

    if(0 == pid)
    {
        signal(SIGUSR1, handler);
        printf("this is child process %d\n", getpid());
        count++;
    }
    else
    {
        printf("this is father process %d\n", getpid());
        count++;
    }
    printf("count=%d\n", count);

    return 0;
}
// 结果输出:
this is father process 12842
count=1
this is child process 12843
count=1

在语句pid = fork();之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程几乎完全相同,我们可以根据fork()的返回值来在两个进程中执行不同的任务。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值

  1. 在父进程中,fork返回新创建子进程的进程ID
  2. 在子进程中,fork返回0;
  3. 如果出现错误,fork返回一个负值。

所以,在fork之后,count变量分别存在于子进程和父进程中,互不干扰。


fork函数执行完毕后,如果创建进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0;在父进程中,fork返回子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
其实就相当于链表,父进程返回的pid指向子进程的进程id,因为子进程没有子进程,所以其pid为0。
fork出错可能有两种原因:

  1. 当前的进程已经达到了系统规定的上限,这是errno的值被设置为EAGAIN
  2. 系统内存不足,这是errno的值被设置为ENOMEM
    创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    每个进程都有一个独特(互不相同)的进程标识符(Process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

更为详细的参考linux中fork()函数详解,这篇博客总结的非常好!

点对点聊天程序实现

实现要点

  • 客户端
    客户端分为两个进程:一个进程用于接收服务器进程发送过来的数据;另一个进程用于接收服务器进程发送过来的数据
  • 服务器
    服务器分为两个进程:一个进程用于接收客户端进程发送过来的数据;另一个进程用于接收客户端进程发送过来的数据

实现代码

  • p2pcli.c
#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <signal.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define ERR_EXIT(m)	\
		do	\
		{	\
			perror(m);		\
			exit(EXIT_FAILURE);	\
		}while(0)

void handler(int sig)
{
	printf("recv s sig=%d\n", sig);
	exit(EXIT_SUCCESS);
	
}			
		
int main(void)
{
	int sock;
	sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//	sock = socket(AF_INET, SOCK_STREAM, 0);
	if(sock <0)
		ERR_EXIT("socket");
	
	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");	// 服务器端地址

	if(connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
	{
		ERR_EXIT("connect");
	}
	
	pid_t pid;
	pid = fork();
	if(-1 == pid)
	{
		ERR_EXIT("fork");
	}
	else if(0 == pid)
	{
		char recvBuf[1024];
		while(1)
		{
			memset(recvBuf, 0, sizeof(recvBuf));
			int ret = read(sock, recvBuf, sizeof(recvBuf));
			if(-1 == ret)
			{
				ERR_EXIT("read");
			}
			else if(0 == ret)
			{
				printf("peer close\n");
				break;
			}
			fputs(recvBuf, stdout);
		}
		close(sock);
		
		kill(getppid(), SIGUSR1);
	}
	else
	{
		signal(SIGUSR1, handler);
		char sendBuf[1024] = {0};
		while(fgets(sendBuf, sizeof(sendBuf), stdin) != NULL)
		{
			write(sock, sendBuf, strlen(sendBuf));
			memset(sendBuf, 0, sizeof(sendBuf));
		}
		close(sock);
	}
	
	return 0;
}
  • p2psvr.c
#include <stdio.h>
#include <unistd.h>

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <signal.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define ERR_EXIT(m)	\
		do	\
		{	\
			perror(m);		\
			exit(EXIT_FAILURE);	\
		}while(0)

void handler(int sig)
{
	printf("recv a sig=%d\n", sig);
	exit(EXIT_SUCCESS);
}
/********
1.问题:不能处理多个客户端连接服务器
 ********/
int main(void)
{
	int listenfd;
	listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if(listenfd <0)
		ERR_EXIT("socket");
	
	struct sockaddr_in servaddr;
	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);		// 绑定本机的任意地址 网络字节序
//	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//	inet_aton("127.0.0.1", &servaddr.sin_addr);

// 没有这个会出现bind: Address already in use
#if 1
	int on = 1;
	if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
	{
		ERR_EXIT("setsockopt");
	}
#endif

	if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
	{
		ERR_EXIT("bind");
	}
	
	if(listen(listenfd, SOMAXCONN) < 0)
	{
		ERR_EXIT("listen");
	}
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen = sizeof(peeraddr);
	int conn;
	// 原因在于服务器端无法接收第二个客户端的连接请求,第一次请求之后就直接进入了和第一个客户端响应的程序中
	if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)	// conn表示已连接套接字
	{
		ERR_EXIT("accept");
	}
	
	printf("ip=%s, port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
	
	pid_t pid;
	pid = fork();
	if(-1 == pid)
	{
		ERR_EXIT("fork");
	}
	
	if(0 == pid)
	{
		// 通知子进程关闭
		signal(SIGUSR1, handler);
		char sendBuf[1024] = {0};
		while(fgets(sendBuf, sizeof(sendBuf), stdin) != NULL)
		{
			write(conn, sendBuf, strlen(sendBuf));
			memset(sendBuf, 0, sizeof(sendBuf));
		}
		exit(EXIT_SUCCESS);
	}
	else
	{
		char recvBuf[1024];
		while(1)
		{
			memset(recvBuf, 0, sizeof(recvBuf));
			int ret = read(conn, recvBuf, sizeof(recvBuf));
			if(-1 == ret)
			{
				ERR_EXIT("read");
			}
			else if(0 == ret)
			{
				printf("peer close\n");
				break;
			}
			fputs(recvBuf, stdout);		
		}
		kill(pid ,SIGUSR1);
		exit(EXIT_SUCCESS);
	}

	close(conn);
	close(listenfd);
	
	return 0;
}

总结

所有的学习都贵在坚持!!!

链接地址

setsockopt()函数功能介绍
setsockopt和getsockopt函数解析
linux中fork()函数详解

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值