基于TCP协议的服务器/客户端程序

作为传输层的主要协议,TCP协议不仅可以支持本地的数据通信,还可以支持跨网络的进程间通信。

在偌大的互联网中,我们可以通过“IP地址+端⼜号”标识互联网中唯一的一个进程。然而,“IP地址+端⼜号”就称为socket,这就是网络socket编程

在TCP协议中,建⽴连接的两个进程各⾃有⼀个socket来标识,那么这两个socket组成 的socketpair就唯⼀标识⼀个连接。

socket本⾝有“插座”的意思,因此⽤来描述⽹络连接的⼀ 对⼀关系。


要写出一个基于TCP的网络服务程序,我们应该具有以下的知识:

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到⾼的顺序发出,接收主机把从⽹络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到⾼的顺序保存。
  2. TCP/IP协议规定,⽹络数据流应采⽤⼤端字节序,即低地址⾼字节。
  3. socket API是⼀层抽象的⽹络编程接⼜,适⽤于各种底层⽹络协议,如IPv4、 IPv6,以及后⾯要讲的UNIX Domain Socket。

       IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址⽤sockaddr_in结构体表⽰,包括16位端⼜号和32位IP地址,IPv6地址⽤sockaddr_in6结构体表⽰,包括16位端⼜号、 128位IP地址和⼀些 控制字段。 UNIX Domain Socket的地址格式定义在sys/un.h中,⽤sockaddr_un结构体表⽰。各 种socket地址结构体的开头都是相同的,前16位表⽰整个结构体的长度(并不是所有UNIX的实现 都有长度字段,如Linux就没有),后16位表⽰地址类型。 IPv4、 IPv6和UNIXDomain Socket的地 址类型分别定义为常数AF_INET、 AF_INET6、 AF_UNIX。这样,只要取得某种sockaddr结构体的 ⾸地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的 内容。因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例 如bind、 accept、 connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指 针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都 ⽤struct sockaddr *类型表⽰,在传递参数之前要强制类型转换⼀下.结构如图所示:

基于TCP协议的服务器/客户端程序_第1张图片
通常⽤点分⼗进制的字符串表⽰IP 地址,以下函数可以在字符串表⽰ 和in_addr表⽰之间转换。
字符串转in_addr的函数:


in_addr转字符串的函数:


以下是一次客户端,服务端从请求连接到断开连接的依次完整会话过程

基于TCP协议的服务器/客户端程序_第2张图片




       服务器调⽤socket()、 bind()、 listen() 完成初始化后,调⽤accept()阻塞等待,处于监听端⼜的状态,客户端调⽤socket()初始化后,调⽤connect()发出SYN段并阻塞等待服务器应答,服务器应答⼀个SYN-ACK段,客户端收到后从connect()返回,同时应答⼀个ACK段,服务器收到后 从accept()返回。


数据传输的过程: 建⽴连接后,TCP协议提供全双⼯的通信服务,但是⼀般的客户端/服务器程序的流程是由客户端主 动发起请求,服务器被动处理请求,⼀问⼀答的⽅式。因此,服务器从accept()返回后⽴刻调 ⽤read(),读socket就像读管道⼀样,如果没有数据到达就阻塞等待,这时客户端调⽤write()发送 请求给服务器,服务器收到后从read()返回,对客户端的请求进⾏处理,在此期间客户端调 ⽤read()阻塞等待服务器的应答,服务器调⽤write()将处理结果发回给客户端,再次调⽤read()阻塞 等待下⼀条请求,客户端收到后从read()返回,发送下⼀条请求,如此循环下去。如果客户端没有更多的请求了,就调⽤close() 关闭连接,就像写端关闭的管道⼀样,服务器 的ead()返回0,这样服务器就知道客户端关闭了连接,也调⽤close()关闭连接。注意,任何⼀⽅调⽤close() 后,连接的两个传输⽅向都关闭,不能再发送数据了。如果⼀⽅调⽤shutdown() 则连接处 于半关闭状态,仍可接收对⽅发来的数据。


有了上面的分析过程,不难写出一下程序:

(一)server.c 的作⽤是接受client的请求,并与client进⾏简单的数据通信,整体为⼀个阻塞式的⽹络聊天⼯具。

#include
#include
#include
#include
#include
#include
#include


static void Usage(const char* proc)
{
	printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		Usage(argv[0]);
	//	exit(1);
		return 4;
	}
	int listen_sock=socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock<0)
	{
		perror("socket");
		return 1;
	}
	struct sockaddr_in local;
	local.sin_family=AF_INET;
	local.sin_port=htons(atoi(argv[2]));
	local.sin_addr.s_addr=inet_addr(argv[1]);
	if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
	{
		perror("bind");
		return 2;
	}
	listen(listen_sock,5);//max listen bind 5
	struct sockaddr_in peer;
	socklen_t len=sizeof(peer);
	int fd=accept(listen_sock,(struct sockaddr*)&peer,&len);
	if(fd<0)
	{
		perror("accept");
		return 3;
	}



	char buf[1024];
	while(1)
	{
		memset(buf,'\0',sizeof(buf));
		ssize_t _s=read(fd,buf,sizeof(buf)-1);
		if(_s>0)
		{
			buf[_s]='\0';
			printf("client :");
			printf("%s\n",buf);
			write(fd,buf,strlen(buf));
			printf("server :%s\n",buf);
		}else
		{
			printf("read done....\n");
			break;
		}
	}
	return 0;
}

socket()打开⼀个⽹络通讯端⼜,如果成功的话,就像open()⼀样返回⼀个⽂件描述符,应⽤程序可以像读写⽂件⼀样⽤read/write在⽹络上收发数据,如果socket()调⽤出错则返回-1。对 于IPv4,family参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表⽰⾯向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表⽰⾯向数据报的传输协 议。 protocol参数的介绍从略,指定为0即可。

bind()的作⽤是将参数sockfd和myaddr绑定在⼀起,使sockfd这个⽤于⽹络通讯的⽂件描述符监听myaddr所描述的地址和端⼜号。

listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。

三⽅握⼿完成后,服务器调⽤accept()接受连接,如果服务器调⽤accept()时还没有客户端的连接请 求,就阻塞等待直到有客户端连接上来。 cliaddr是⼀个传出参数,accept()返回时传出客户端的地 址和端⼜号。 addrlen参数是⼀个传⼊传出参数(value-result argument),传⼊的是调⽤者提供的 缓冲区cliaddr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调⽤者提供的缓冲区)。如果给cliaddr 参数传NULL,表⽰不关⼼客户端的地址。

(二)client.c的作⽤是链接server,并向server发起通信请求。
#include
#include
#include
#include
#include
#include
#include

static void Usage(const char* proc)
{
	printf("Usage:%s[ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
	if(argc!=3)
	{
		Usage(argv[0]);
	//	exit(1);
		return 4;
	}
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("socket");
		return 1;
	}
	struct sockaddr_in remote;
	remote.sin_family=AF_INET;
	remote.sin_port=htons(atoi(argv[2]));
	remote.sin_addr.s_addr=inet_addr(argv[1]);
	if(connect(sock,(struct sockaddr*)&remote,sizeof(remote))<0)
	{
		perror("connect");
		return 2;
	}
	while(1)
	{
		char buf[1024];
		memset(buf,'\0',sizeof(buf));
		printf("client :");
		fflush(stdout);
		ssize_t _s=read(0,buf,sizeof(buf)-1);
		if(_s>0)
		{
			buf[_s-1]='\0';
			write(sock,buf,strlen(buf));
			memset(buf,'\0',sizeof(buf));
			_s=read(sock,buf,sizeof(buf));
			printf("server :");
			printf("%s\n",buf);

		}else
		{
			printf("read done....\n");
			break;
		}


	}
	return 0;
}
    由于客户端不需要固定的端⼜号,因此不必调⽤bind(),客户端的端⼜号由内核⾃动分配。注意, 客户端不是不允许调⽤bind(),只是没有必要调⽤bind()固定⼀个端⼜号,服务器也不是必须调⽤bind(),但如果服务器不调⽤bind(),内核会⾃动给服务器分配监听端⼜,每次启动服务器时端⼜ 号都不⼀样,客户端要连接服务器就会遇到⿇烦。

    客户端需要调⽤connect()连接服务器,connect和bind的参数形式⼀致,区别在于bind的参数是⾃⼰的地址,⽽connect的参数是对⽅的地址。 connect()成功返回0,出错返回-1。

我们来看一下server和client是怎样通信的(运行截图):
基于TCP协议的服务器/客户端程序_第3张图片
(一)先运行服务器:

服务器阻塞等待客户机请求链接。


(二)重启一个终端,然后运行客户机:


(三)然后客户机发送数据:
基于TCP协议的服务器/客户端程序_第4张图片

(四)服务器接到数据,并回显给客户机:
基于TCP协议的服务器/客户端程序_第5张图片



这里有一个问题,当我们ctrl+Z暂停服务器,然后再起一个服务时,它会提醒我们Address already in use。这就像服务器挂了


client终⽌时⾃动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状 态。 TCP协议规定,主动关闭连接的⼀⽅要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终⽌了server,所以server是主动关闭连接的⼀⽅,在TIME_WAIT期间仍然不能再次监听同样的server端 ⼜。 MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Linux上⼀般经过半分钟后 就可以再次启动server了。


解决方法是:在socket调用和bind调用之间加上一段对socket的设置:
   int opt = 1;
   setsockopt(socket_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))


你可能感兴趣的:(操作系统)