【Linux Socket 编程入门】05 - 拉个骡子溜溜:TCP编程模型代码分析

(一) 看看以前学了啥

前面介绍了socket的分类,IP地址,端口号(port),常用的socket数据结构以及常用的函数。现在我们来看一个例子,看看socket编程究竟是什么。

(二) 一图看懂客户服务器模型(Client-Server)

在开始编码之前,先简单了解一下客户服务器模型。在网络的世界里,几乎到处都可以看到客户服务器模型。通常,主动发起连接请求的一端称为客户端,被动响应连接的一段称为服务器端。比如最常见的一种客户服务器模型的应用,web浏览器(web client)就是一种客户端程序,你在浏览器里面输入想要访问的网站,浏览器就会主动connect网站对应的服务器(web server),服务器传回网站相关的信息,浏览器显示在页面里面,这样就完成了客户服务器之间的数据交换。

常见的客户服务器模型的应用还有:telnet/telnetd, ftp/ftpd, or Firefox/Apache。通常情况下,只有一个服务器端程序处理来自多个不同的客户端程序的请求,当有新的请求到来时,服务器端会利用fork()系统调用,新建一个进程来处理与客户端的通信。我们接下来的代码分析同样也是客户服务器模型。

(三) 上个菜:一个TCP程序分析

开胃:TCP客户服务器编程模型

首选,我们看一下TCP的客户服务器编程模型,这对于我们理解后面的程序非常有帮助。 这是本文的重点,理解了这幅图,也就基本理解了TCP编程的原理了


上菜:TCP编程实例分析

服务器端程序:(可以参照上面的编程模型,理解下面这段代码)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#define PORT "3490" // 定义通信的端口号为3490.注意端口号要大于1024
#define BACKLOG 10 // listen()的第二个参数,定义可以同时等待的connection的最大数值。也就是在accept()前,最多只能有10个人请求连接。


//根据IP地址的类型,将sockaddr结构体类型转换,获取相对应的IP地址。IPv4还是IPv6.
void *get_in_addr(struct sockaddr *sa)
{
	if(sa->sa_family == AF_INET) {
		return &(((struct sockaddr_in *)sa)->sin_addr); //IPv4
	}
	return &(((struct sockaddr_in6 *)sa)->sin6_addr); // IPv6
}

int main(void)
{
	int sockfd, new_fd;
	struct addrinfo hints, *servinfo, *p;
	struct sockaddr_storage their_addr;
	socklen_t sin_size;
	struct sigaction sa;
	int yes = 1;
	char s[INET6_ADDRSTRLEN];
	int rv;
	
	memset(&hints, 0, sizeof(hints)); //初始化结构体。因为我们并没有使用结构体里面的每一个成员,因此在使用之前,一定要先将整个结构体清0.
	hints.ai_family = AF_UNSPEC;  //由系统选择是IPv4还是IPv6.
	hints.ai_socktype = SOCK_STREAM; //TCP属于stream socket。
	hints.ai_flags = AI_PASSIVE;  //由系统在bind()的时候,自动填充本机IP地址。
	if((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0) {
		fprintf(stderr, "getaddrinfo:%s\n", gai_strerror(rv));
		return 1;
	}
	
	//因为getaddrinfo的返回值servinfo是一个结构体,前面我们有讲到,
	//对于同一个主机名,有可能会返回多个IP地址。因此我们这里采用轮询的方式
	//选择第一个可用的IP地址。
	for(p = servinfo; p != NULL; p = p->ai_next) {
		if(-1 == (sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))) {  // socket()的参数都是来自getaddrinfo()返回的servinfo结构体。
			perror("server: socket"); 
			continue;
		}

		if(-1 == (bind(sockfd, p->ai_addr, p->ai_addrlen))) {  // bind()的参数同样也是来自getaddrinfo()返回的servinfo结构体。
			close(sockfd);                //这一步很重要,记得如果bind失败,一定要close释放掉之前分配的文件描述符。
			perror("server: bind");
			continue;
		}

		break;   //当找到一个可用的IP以后,跳出循环。
	}	

	freeaddrinfo(servinfo); // all done with this structure.
	
	if(NULL == p) { //如果一个可用的IP都没有,失败,退出程序。
		fprintf(stderr, "server: failed to bind\n");
		exit(1);
	}	
	
	if(-1 == listen(sockfd, BACKLOG)) { //设置可以同时等待连接的最大值,开启倾听模式。
		perror("listen");
		exit(1);
	}

	printf("server: waiting for connections...\n");

	while(1) {
		sin_size = sizeof(their_addr);
		new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); //等待连接,accept()函数会一直等待,直到有人连接才会返回。
		if(-1 == new_fd) { //accept()函数返回会分配一个新的socket文件描述符。
			perror("accept");
			continue;
		}

		inet_ntop(their_addr.ss_family,
					get_in_addr((struct sockaddr *)&their_addr),
					s,
					sizeof(s)); // 解析对方IP地址。
		printf("server: got connection from %s\n", s);

		if(!fork()) {  //创建新的子进程处理新的连接请求。
			close(sockfd); // 子进程不再需要原来的sockfd
			if(-1 == (send(new_fd, "Hello, world!", 13, 0))) { //向客户端发送消息:Hello, world!
				perror("send");
			}
			close(new_fd); //不再使用,一定要close()掉。
			exit(0);
		}
		close(new_fd); // 父进程不需要处理这个新的文件描述符。close()掉。
			
	}

	return 0;
}
客户端程序:(可以参照上面的编程模型,理解下面这段代码)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>

#define PORT "3490" // 想要连接的服务器端的端口号,注意这个端口号需要跟服务器端一样
#define MAXDATASIZE 100 // 一次可以读取的最大的数据量。

//根据IP地址的类型,将sockaddr结构体类型转换,获取相对应的IP地址。IPv4还是IPv6.
void *get_in_addr(struct sockaddr *sa)
{
	if(sa->sa_family == AF_INET) {
		return &(((struct sockaddr_in *)sa)->sin_addr);
	}
	return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}

int main(int argc, char *argv[])
{
	int sockfd, numbytes;
	char buf[MAXDATASIZE];
	struct addrinfo hints, *servinfo, *p;
	int rv;
	char s[INET6_ADDRSTRLEN];

	if(2 != argc) {  //客户端程序需要在命令行传入服务器端的IP地址
		fprintf(stderr, "usage: client hostname\n");
		exit(1);
	}

	memset(&hints, 0, sizeof(hints)); //初始化结构体。因为我们并没有使用结构体里面的每一个成员,因此在使用之前,一定要先将整个结构体清0.
	hints.ai_family = AF_UNSPEC; //由系统选择是IPv4还是IPv6.
	hints.ai_socktype = SOCK_STREAM; //TCP属于stream socket。
	//hints.ai_flags = AI_PASSIVE;  //在客户端,不需要设定这个flag,因为我们会自己指定服务器的IP地址。

	if(0 != (rv = getaddrinfo(argv[1], PORT, &hints, &servinfo))) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}
	
	//因为getaddrinfo的返回值servinfo是一个结构体,前面我们有讲到,
	//对于同一个主机名,有可能会返回多个IP地址。因此我们这里采用轮询的方式
	//选择第一个可用的IP地址。
	for(p = servinfo; p != NULL; p = p->ai_next) {
		if(-1 == (sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol))) { // socket()的参数都是来自getaddrinfo()返回的servinfo结构体。
			perror("client: socket");
			continue;
		}


		if(-1 == (connect(sockfd, p->ai_addr, p->ai_addrlen))) {  // 客户端主动发起连接请求。目的地IP地址,端口号都在p->ai_addr里面。
			close(sockfd);  //这一步很重要,记得如果bind失败,一定要close释放掉之前分配的文件描述符。
			perror("client: connect");
			continue;

		}

		break; //当找到一个可用的IP以后,跳出循环。
	}
	
	if(NULL == p) {  //如果一个可用的IP都没有,失败,退出程序。
		fprintf(stderr, "client: failed to connect\n");
		return 2;
	}	

	inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof(s)); //解析IP地址
	printf("client: connecting to %s\n", s);
	freeaddrinfo(servinfo); // 不再使用servinfo,释放。
	if(-1 == (numbytes = recv(sockfd, buf, MAXDATASIZE - 1, 0))) { //一旦connect成功,就可以从服务器端接收数据。recv()会一直等待,知道有数据到达才会退出。
		perror("recv");
		exit(1);
	}
	buf[numbytes] = '\0'; //最后一个byte设为结束符。
	printf("client: received '%s' \n", buf);
	
	close(sockfd); //不再使用的时候,释放。很重要。

	return 0;
}

测试步骤:
测试的环境为:ubuntu 16.04

利用gcc分别编译客户端程序和服务器端程序。编译完成后,先启动服务器端程序(假设在当前目录下),在shell下执行:
./server
然后再启动客户端程序:打开另一个终端,在shell中执行(利用loop back IP address进行测试):
./client 127.0.0.1

测试结果:

在服务器端会看到如下信息:
server: waiting for connections...
server: got connection from 127.0.0.1
在客户端会看到如下信息:
client: connecting to 127.0.0.1
client: recevied 'Hello World!'

--THE END--

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值