书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现

Section I Problem Specification

实验要求:

本次实验是实现一个简单的C/S模型,是基于客户端和服务端的网络编程。服务端的程序启动后,会在在某端口监听来自客户端的连接,连接后,客户端会发送一个条字段给服务端,服务端接到字段后打印出来,然后再把接收到的字段返还给客户端,算是完成了一次完整的通信。此外,服务器不只只能为一个客户端服务,也就是,有多少不同的客户端连接服务端,服务端都需要应答。这里涉及了服务端的并发编程,在这里我们使用多线程来满足要求:主线程监听连接,每当有一个来自客户端的连接到来时,主线程新开一个线程与客户端通信。

基础知识:

客户端-服务器的通信:

实际上两者之间是客户端的一个进程和服务器的一个进程在通信,并且都通过了各自固定的端口。
此外,通过这种方式,服务器可以给客户端提供服务,这是因为服务器拥有丰富的资源。
一次提供服务的过程称为:transaction
如下图所示:

书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第1张图片



I/O设备——网络:

如下图所示,网络只是一台主机的一个I/O设备而已,就像连接到I/O总线上的其他设备:键盘、显示器。只是一直数据的发送处或者接受处(serves as a source and sink
for data)。
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第2张图片


数据的流动:

较小的局域网,如下图所示,是用连接主机的双绞线和集线器(hub )的玩意组成,并称之为:以太网段。
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第3张图片
较大的局域网,如下图所示,是由网桥将不同的以太网段组成起来。网桥的更多解释请看书:616页
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第4张图片


上两幅图我们经常简化如下图,代表一个局域网:



那么互联网需要我们利用路由器来连接,如下图所示:
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第5张图片



现在看起来我们打通了所有的主机,利用集线器、网桥、路由器,但是这并不代表主机之间的数据就可以随意串流。为了数据的传达到该到的地方,而且能够完整的传达到。需要很多不同的协议,在本次通信中是使用了TCP协议,当然路由器之间也有更多的协议帮助我们传播数据,如下图所示,对于我们要传播的data,还需要很多一定的加工,符合协议要求才能传送到对的地方。
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第6张图片



TCP/IP的数据传输:

每台主机都能利用TCP/IP协议传播数据,这里因为现代操作系统都支持这个协议,一幅图展现了硬件和软件的结合,来利用TCP/IP传输数据:

书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第7张图片

Section II Solution Method and Design

套接字接口:

不同的操作系统下,都提供了套接字的接口,这些套接字的接口就相当于上一幅图中的内核代码,已经被开发操作系统的人写好了,我们只需要调用api即可。下衣服中展示了各个套接字的函数调用顺序和处理过程:
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第8张图片
虽然上图大多数都是unix下的api,但是我相信,依次理解windows的流程也是没有问题的。


关于我们如何在编码时存放Ip地址以及端口等东西,也是很有必要阐述的,主要是使用了两个套接字结构,unix和windows下通用:
书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第9张图片
其中,connect、bind和accept是需要需要第一个套接字结构:sockaddr。两个套接字结构可以通过强制类型转换相互转化。

Unix下echo服务器和客户杜的实现

客户端代码:

重要处已经标示了注释
1 #include "csapp.h"//是书提供的头文件
2
3 int main(int argc, char **argv)
4 {
5 int clientfd, port;
6 char *host, buf[MAXLINE];
7 rio_t rio;
8
9 if (argc != 3) {
10 fprintf(stderr, "usage: %s  \n", argv[0]);
11 exit(0);
12 }
13 host = argv[1];
14 port = atoi(argv[2]);
15
16 clientfd = Open_clientfd(host, port);//根据输入的ip和端口连接相应的服务器,如果连接成功将会返回一个int型的数值代表该通道,代码中就是clientfd
17 Rio_readinitb(&rio, clientfd);//做一个读取的准备工作,表示从连接的的通道clientfd中读取,并会将读到的内容放到rio里面去
18
19 while (Fgets(buf, MAXLINE, stdin) != NULL) {//从标准的输入中读取内容,存放到缓冲区,buf里面
20 Rio_writen(clientfd, buf, strlen(buf));//缓冲区内容buf通过clientfd通道传输给服务器
21 Rio_readlineb(&rio, buf, MAXLINE);//clinetfd传来的内容会放到rio里面,这句话就是将在rio里面读一行,并且放到buf里面去。
22 Fputs(buf, stdout);//将buf里面的内容放到stdout,标准的输出,一般情况下就是屏幕。
23 }
24 Close(clientfd);//关闭掉这个连接的通道
25 exit(0);
26 }

服务器代码:

1 #include "csapp.h"
2
3 void echo(int connfd);
4
5 int main(int argc, char **argv)
6 {
7 int listenfd, connfd, port, clientlen;
8 struct sockaddr_in clientaddr;
9 struct hostent *hp;
10 char *haddrp;
11 if (argc != 2) {
12 fprintf(stderr, "usage: %s \n", argv[0]);
13 exit(0);
14 }
15 port = atoi(argv[1]);
16
17 listenfd = Open_listenfd(port);//创建一个监听,并且返回一个int类型作为监听标示符(descriptor)
18 while (1) {
19 clientlen = sizeof(clientaddr);
20 connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);//用监听创建一个"等待连接的状态",程序会阻塞在此处,直到有客户端来连接,连接会返回一个int类型的connfd,作为与这个客户端连接的标示符
21
22 /* Determine the domain name and IP address of the client */
23 hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
24 sizeof(clientaddr.sin_addr.s_addr), AF_INET);//利用客户端的信息返回这个客户端的名字
25 haddrp = inet_ntoa(clientaddr.sin_addr);//将ip转为点分格式,用于显示
26 printf("server connected to %s (%s)\n", hp->h_name, haddrp);//打印连接的客户端的名字和ip
27
28 echo(connfd);//echo函数见下
29 Close(connfd);
30 }
31 exit(0);
32 }

1 #include "csapp.h"
2
3 void echo(int connfd)
4 {
5 size_t n;
6 char buf[MAXLINE];
7 rio_t rio;
8
9 Rio_readinitb(&rio, connfd);//准备从connfd的通道里面读内容,读的内容放到rio缓冲区
10 while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {//从rio中读一行放到buf中
11 printf("server received %d bytes\n", n);
12 Rio_writen(connfd, buf, n);//再把收的到内容写到连接通道中connfd
13 }
14 }



Windows下echo服务器和客户段的实现:

这部分大概与上一上小节内容类似,无论是大概流程还是代码,所以就在这里具体分析了,具体的代码请看最后一大节。
是我的实现的代码,而且还实现了简单的多线程。


并发编程

参考书中649页。

下面的代码是一个基于线程的并发服务器,基本的思想是,每有一个客户端连接上了服务器,那么就会产生一个新的线程,那么这个新的线程负责与连接的客户端处理事务。

1 #include "csapp.h"
2
3 void echo(int connfd);
4 void *thread(void *vargp);//处理与客户端通信的线程
5
6 int main(int argc, char **argv)
7 {
8 int listenfd, *connfdp, port;
9 socklen_t clientlen=sizeof(struct sockaddr_in);
10 struct sockaddr_in clientaddr;
11 pthread_t tid;
12
13 if (argc != 2) {
14 fprintf(stderr, "usage: %s \n", argv[0]);
15 exit(0);
16 }
17 port = atoi(argv[1]);
18
19 listenfd = Open_listenfd(port);
20 while (1) {
21 connfdp = Malloc(sizeof(int));
22 *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen);//每当接到一个client的连接后
23 Pthread_create(&tid, NULL, thread, connfdp);//产生一个线程,将执行thread函数的内容。
24 }
25 }
26
27 /* Thread routine */
28 void *thread(void *vargp)
29 {
30 int connfd = *((int *)vargp);
31 Pthread_detach(pthread_self());//分离自己这个线程,这样的话,当执行完这个代码后,线程会自我销毁
32 Free(vargp);
33 echo(connfd);//echo方法请看上一节
34 Close(connfd);
35 return NULL;
36 }

windows的处理方式与上述代码类型,代码请看最后一大节。


Section III Test Cases and Results Analysis

书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第10张图片

书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第11张图片

Section IV Conclusion

  1. 由于这本书写的非常清楚,到没有太大疑惑。

  2. 对于并发编程,是一件非常难得事,一般有三个并发编程的方式
    基于进程的并发编程:我怎么感觉不常用
    基于I/O多路复用的并发编程:这个比较难懂,我不是特别理解
    基于线程的并发编程:最常用,我个人认为比较好理解,关键还比较好的解决了数据共享的问题

  3. 本书还讲了一个重要的理念,就是基于预线程化的并发服务器(P674),我认为就这个就是常说的线程池的技术。
    大概意思就是说:先初始化几个线程,这几个线程一直负责处理与客户端的连接,当连接的客户端超过了线程数的话,不会创建新的线程,这样能够开发出稳定的服务器。
    下面一幅图表明了这个思想:书:深入理解计算机系统(P614) 之 网络编程:简单echo客户端和服务端的实现_第12张图片

Section V References

深入理解计算机系统(原书第2版) 作者: (美)Randal E.Bryant / David O'Hallaron 译者: 龚奕利 / 雷迎春 出版年: 2010年

Section VI Appendix

服务器端代码:

#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 
#include 
#include  
// Need to link with Ws2_32.lib
#pragma comment (lib, "Ws2_32.lib")
//#define DEFAULT_PORT "27015"
// #pragma comment (lib, "Mswsock.lib")

#define DEFAULT_BUFLEN 512
typedef struct _MyData {
	SOCKET ClientSocket;
} MYDATA, *PMYDATA;

DWORD WINAPI ThreadProc(LPVOID lpParameter){
	PMYDATA pData;
	int iResult,iSendResult;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;
	pData = (PMYDATA)lpParameter;
	SOCKET ClientSocket = pData->ClientSocket;
	// Receive until the peer shuts down the connection
	do {

		iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
		if (iResult > 0) {
			recvbuf[iResult]='\0';//因为是从0开始的,所以如果这里是14,那么第14个为\0
			printf("thread ID: %d Bytes received: %s   %d\n", GetCurrentThreadId(),recvbuf,iResult);
			// Echo the buffer back to the sender
			iSendResult = send( ClientSocket, recvbuf, iResult, 0 );
			if (iSendResult == SOCKET_ERROR) {
				printf("send failed with error: %d\n", WSAGetLastError());
				closesocket(ClientSocket);
				WSACleanup();
				return 1;
			}
			printf("Bytes sent: %d\n", iSendResult);
		}
		else if (iResult == 0)
			printf("Receive Completed...\n");
		else  {
			printf("recv failed with error: %d\n", WSAGetLastError());
			closesocket(ClientSocket);
			WSACleanup();
			return 1;
		}

	} while (iResult > 0);
	while(1){
		Sleep(10000);
		printf("thread ID: %d  working \n", GetCurrentThreadId());
	}

	// shutdown the connection since we're done
	iResult = shutdown(ClientSocket, SD_SEND);
	if (iResult == SOCKET_ERROR) {
		printf("shutdown failed with error: %d\n", WSAGetLastError());
		closesocket(ClientSocket);
		WSACleanup();
		return 1;
	}

	// cleanup
	closesocket(ClientSocket);
}
int __cdecl main(int argc, char **argv) 
{
	PMYDATA pData;
	WSADATA wsaData;
	int iResult;
	char hostName[255];
	PHOSTENT hostinfo; 
	LPCSTR hostIp;
	SOCKET ListenSocket = INVALID_SOCKET;
	//SOCKET ClientSocket = INVALID_SOCKET;
	DWORD dwThreadId;
	HANDLE hThread;

	struct addrinfo *result = NULL;
	struct addrinfo hints;

	int iSendResult;
	char recvbuf[DEFAULT_BUFLEN];
	int recvbuflen = DEFAULT_BUFLEN;

	if (argc != 2) {
		printf("usage: %s server-port\n", argv[0]);
		return 1;
	}
	// Initialize Winsock
	iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
	if (iResult != 0) {
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	ZeroMemory(&hints, sizeof(hints));
	hints.ai_family = AF_INET;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;
	hints.ai_flags = AI_PASSIVE;

	// Resolve the server address and port
	iResult = getaddrinfo(NULL, argv[1], &hints, &result);
	if ( iResult != 0 ) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	// Create a SOCKET for connecting to server
	ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
	if (ListenSocket == INVALID_SOCKET) {
		printf("socket failed with error: %ld\n", WSAGetLastError());
		freeaddrinfo(result);
		WSACleanup();
		return 1;
	}

	// Setup the TCP listening socket
	iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);
	if (iResult == SOCKET_ERROR) {
		printf("bind failed with error: %d\n", WSAGetLastError());
		freeaddrinfo(result);
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	freeaddrinfo(result);

	iResult = listen(ListenSocket, SOMAXCONN);
	if (iResult == SOCKET_ERROR) {
		printf("listen failed with error: %d\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	if( gethostname ( hostName, sizeof(hostName)) == 0) { 

		//如果成功地将本地主机名存放入由name参数指定的缓冲区中 
		if((hostinfo = gethostbyname(hostName)) != NULL) { 

			//这是获取主机名,如果获得主机名成功的话,将返回一个指针,指向hostinfo,hostinfo为PHOSTENT型的变量,下面即将用到这个结构体 
			hostIp = inet_ntoa (*(struct in_addr *)*hostinfo->h_addr_list); 
		}
	}

	// Accept a client socket
	printf("Ip: %s Listening on Port %s \n",hostIp,argv[1]);
	while(1){
		SOCKET ClientSocket = accept(ListenSocket, NULL, NULL);
		if (ClientSocket == INVALID_SOCKET) {
			printf("accept failed with error: %d\n", WSAGetLastError());
			closesocket(ListenSocket);
			WSACleanup();
			return 1;
		}
		pData = (PMYDATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
			sizeof(MYDATA));
		pData->ClientSocket=ClientSocket;
		hThread = CreateThread(
			NULL, // default security attributes
			0, // use default stack size
			ThreadProc, // thread function
			pData, // argument to thread function
			0, // use default creation flags
			&dwThreadId); // returns the thread identifier
		// No longer need server socket
		//closesocket(ListenSocket);

	}
	
	
	WSACleanup();
	system("pause");
	return 0;
}

客户端代码:

#define WIN32_LEAN_AND_MEAN

#include 
#include 
#include 
#include 
#include 


// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define DEFAULT_BUFLEN 512
//#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv) 
{	
	WSADATA wsaData;
	SOCKET ConnectSocket = INVALID_SOCKET;
	struct addrinfo *result = NULL,
		*ptr = NULL,
		hints;
	char *sendbuf = "hello,world!";
	char recvbuf[DEFAULT_BUFLEN];
	int iResult;
	int recvbuflen = DEFAULT_BUFLEN;

	// Validate the parameters
	if (argc != 3) {
		printf("usage: %s server-name\n", argv[0]);
		return 1;
	}

	// Initialize Winsock
	iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
	if (iResult != 0) {
		printf("WSAStartup failed with error: %d\n", iResult);
		return 1;
	}

	ZeroMemory( &hints, sizeof(hints) );
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_protocol = IPPROTO_TCP;

	// Resolve the server address and port
	iResult = getaddrinfo(argv[1], argv[2], &hints, &result);
	if ( iResult != 0 ) {
		printf("getaddrinfo failed with error: %d\n", iResult);
		WSACleanup();
		return 1;
	}

	// Attempt to connect to an address until one succeeds
	for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

		// Create a SOCKET for connecting to server
		ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
			ptr->ai_protocol);
		if (ConnectSocket == INVALID_SOCKET) {
			printf("socket failed with error: %ld\n", WSAGetLastError());
			WSACleanup();
			return 1;
		}

		// Connect to server.
		iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
		if (iResult == SOCKET_ERROR) {
			closesocket(ConnectSocket);
			ConnectSocket = INVALID_SOCKET;
			continue;
		}
		break;
	}

	freeaddrinfo(result);

	if (ConnectSocket == INVALID_SOCKET) {
		printf("Unable to connect to server!\n");
		WSACleanup();
		return 1;
	}

	// Send an initial buffer
	iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
	if (iResult == SOCKET_ERROR) {
		printf("send failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	printf("Bytes Sent: %ld\n", iResult);

	// shutdown the connection since no more data will be sent
	iResult = shutdown(ConnectSocket, SD_SEND);
	if (iResult == SOCKET_ERROR) {
		printf("shutdown failed with error: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	// Receive until the peer closes the connection
	do {

		iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
		if ( iResult > 0 ){
			recvbuf[iResult]='\0';
			if (!(strcmp(sendbuf,recvbuf)))//比对接收字符和发送字符是否相同
			{	//如果相同就打印
				printf("Check completed,the Bytes from server is same as sent content\n");
				printf("Bytes received from Server: %s   %d\n", recvbuf,iResult);
			}
		}
			
		else if ( iResult == 0 )
			printf("Connection closed\n");
		else
			printf("recv failed with error: %d\n", WSAGetLastError());

	} while( iResult > 0 );

	// cleanup
	closesocket(ConnectSocket);
	WSACleanup();

	return 0;
}


你可能感兴趣的:(林老师的作业)