【UNIX网络编程(四)】TCP套接字编程详细分析

引言:

套接字编程其实跟进程间通信有一定的相似性,可能也正因为此,stevens这位大神才会将套接字编程与进程间的通信都归为“网络编程”,并分别写成了两本书《UNP1》《UNP2》。TCP套接字编程是套接字编程中非常重要的一种,仔细分析,其实它的原理并不复杂。现在就以一个例子来详细分析TCP套接字编程。

一、示例要求:

本节中试着编写一个完成的TCP客户/服务器程序示例,并对它进行深入的探讨。该示例会用到绝大多数的基本函数,未用到但比较重要的函数会在后面的补充上。示例要完成的内容是实现一个客户/服务器程序:

1、客户从标准输入读入一行文本,并写给服务器;

2、服务器从网络输入读入这行文本,并回射给客户。

3、客户从网络输入读入这行回射文本,并显示在标准输出上。

用图表示给该客户/服务器示例就是:

大致分析该客户/服务器程序:首先,客户程序通过fgets函数从标准输入得到文本,然后客户程序调用write函数发给服务器。服务器通过read函数读取客户程序发过来的文本,然后服务器调用write函数将“从客户端读取的文本”重新发给客户端。客户端调用read函数读取服务器重新发给客户端的文本,之后客户程序调用fputs函数将读取的文本重新发送到标准输出。

从上面的分析可以看出,TCP客户程序与TCP服务器程序是两个独立的程序,它们可以组合使用从而形成一个“TCP回射客户/服务器程序”。下面我们首先分别写好客户端程序、服务器端程序,并用Socket调试工具,进行调试。调试结束后,组合这两个程序,形成一个客户/服务器程序。

二、示例分析:

在写客户/服务器程序之前,我们先用图来展示一下各个阶段是如何工作的,以及客户、服务器是如何建立连接并传递数据的。

从上到下按照顺序分析客户端应用程序以及服务器段应用程序如下:

1、服务器端调用socket()函数创建一个套接字,由socket()函数创建的套接字都是主动套接字,也就是说,即使我们的初衷是在服务器端创建的套接字,但此时内核并不知道我们的意图,此时的套接字仅仅是一个要调用connect()函数发起连接的客户套接字,那么该如何使它变成一个被动套接字呢?也就是说如何告诉内核应该接受指向该套接字的连接请求呢?必须要经过后面的bind()函数和listen()函数。

2、调用完socket()函数后,TCP此时的状态是CLOSED状态,接下来我们调用bind()函数,该函数完成对ip地址和端口号的绑定。注意bind函数一般是由服务器调用的,客户端一般不会调用该函数绑定地址,因为对于端口号,内核会自动的帮助客户端确定它的端口号为一个临时端口号;对于IP地址,内核将根据所用外出网络接口来选择源IP地址。

3、服务器端此时调用listen()函数,它的任务就是将上面步骤1中socket()函数创建的主动套接字转换为一个被动套接字,指示内核应该接受指定该套接字的连接请求,同时规定了内核应该为相应套接字排队的最大连接数。listen()函数调用完成之后,TCP状态由CLOSED状态转换到LISTEN状态。

4、服务器端调用accept()函数,它用于从已完成连接队列对头返回下一个已完成连接。如果已完成连接队列为空,那么进程休眠。注意,accept()函数的第二三个参数分别用来返回已连接的对端进程(客户)的协议地址和套接字地址结构的长度。如果成功,它的返回值是由内核自动生成的一个全兴描述符,代表与所返回客户的TCP连接。accept()函数的第一个参数就是socket函数创建的套接字,我们称之为监听套接字,返回值为已连接套接字描述符。

5、因为accept()函数会使服务器进程阻塞,等待客户端的连接。客户端用socket()函数创建一个套接字后,接着客户进程调用connect()函数建立与TCP服务器的连接(注意,客户端没用bind、listen函数)。对于TCP套接字,connect函数会激发TCP的三路握手过程,即,客户端发SYN给服务器,服务器接到后回发SYN和ACK给客户端,客户端接受到服务器的SYN后,回发ACK给服务器端。三路握手完成后,连接建立。此时的客户端与服务器端都处于ESTABLISHED状态。连接建立后,我们就可以通过read和write函数在客户和服务器端进行文本的传送了。

6、当文本发送完毕后,客户给服务器端发送一个FIN结束标志,服务器端接到FIN后给客户端回发一个FIN结束标志,然后客户接到服务器的FIN,在给服务器发送一个确认标志ACK。之后,连接断开。两端的TCP状态都处于CLOSED状态。

三、示例代码及讲解

首先给出客户端的程序并用TCP/UDP Socket调试工具进行调试,并给出结果:

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define MAXLINE 2048

int
main(int argc, char **argv)
{
	int sockfd;
	char sendline[MAXLINE], recvline[MAXLINE];
	struct sockaddr_in	cliaddr;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);/*创建套接字*/
	if(sockfd < 0){
		printf("socket function fail.\n");
		return -1;
	}

	bzero(&cliaddr, sizeof(cliaddr));
	cliaddr.sin_family = AF_INET;
	cliaddr.sin_port = htons(9877);
	inet_pton(AF_INET, argv[1], &cliaddr.sin_addr);/*对套接字地址结构进行操作*/

	connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));/*建立连接*/

	while(fgets(sendline, MAXLINE, stdin) != NULL){
		write(sockfd, sendline, strlen(sendline));	/*利用套接字进行数据发送*/
	}
}

编译该程序:gcc client.c -o client

用调试工具进行调试,用该工具建立TCP服务器,端口号设为9877,然后执行上面生成的可执行程序./client 59.73.166.231(该IP地址,是我电脑的IP地址,Linux的IP地址是59.73.166.32,它会出现在调试工具的客户端地址部分)。并在客户端发送数据,abc,然后就会在调试工具里显示该文本abc,接着发送def,同样会在调试工具里出现def文本。效果如下图所示:

由上面可以看出客户端的端口号是53633,它是由内核自动分配的。

下面给出服务器端的程序如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>

char c[2048];

int
main(int argc, char **argv)
{
	int 	n;
	int		sockfd, coundfd;
	struct sockaddr_in serv, client;
	socklen_t socklen;

	/*creat sockfd using the socket function*/
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd == -1){
		printf("socket function fail.\n");
		return -1;
	}

	/*bind the ip addr and port using the bind function*/
	bzero(&serv, sizeof(serv));
	serv.sin_family 	  = AF_INET;
	serv.sin_addr.s_addr  = htonl(INADDR_ANY);
	serv.sin_port		  = 9877;
	if(bind(sockfd, (struct sockaddr *)&serv, sizeof(struct sockaddr_in))){
		printf("bind function fail.\n");
		return -1;
	}

	/*listen function*/
	if(listen(sockfd, 5)){
		printf("listen function fail.\n");
		return -1;
	}

	/*accept function*/
	socklen = sizeof(struct sockaddr_in);
	printf("accept wait \n");
	coundfd = accept(sockfd, (struct sockaddr *)&client, &socklen);
	printf("accept wait \n");

	while(1){
		if((n = read(coundfd, c, 2048) > 0)){
			write(coundfd, c, n);
		}
	}

}

跟上面的客户端程序一样,也可以进行同样的服务器端程序的测试,此处略去了。

四、完成的TCP回射客户/服务器程序

由于上面的程序是进行的测试,客户端并没有回射部分,下面的代码给出回射部分。

客户程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

#define MAXLINE 2048
#define SERV_PORT 9877

void str_cli(FILE *, int);

int
main(int argc, char **argv)
{
	int 	sockfd;
	struct  sockaddr_in cliaddr;

	if(argc != 2){
		printf("usage:tcpcli <IPaddress>");
		return -1;
	}

	sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if(sockfd == -1){
		printf("socket fail.\n");
		return -1;
	}

	bzero(&cliaddr, sizeof(cliaddr));
	cliaddr.sin_family = AF_INET;
	cliaddr.sin_port   = htons(SERV_PORT);
	if(!(inet_pton(AF_INET, argv[1], &cliaddr.sin_addr))){
		printf("inet pton fail.\n");
		return -1;
	}

	if(connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr))){
		printf("connect fail.\n");
		return -1;
	}

	str_cli(stdin, sockfd);

	exit(0);

}

void
str_cli(FILE *fp, int sockfd)
{
	char 	sendline[MAXLINE], recvline[MAXLINE];

	while(fgets(sendline, MAXLINE, fp) != NULL){
		write(sockfd, sendline, strlen(sendline));

		if(read(sockfd, recvline, MAXLINE) == 0)
			printf("str_cli:server terminated prematurely");

		fputs(recvline, stdout);
	}
}

服务器程序:

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define SERV_PORT	9876
#define MAXLINE		2048

void
str_echo(int sockfd)
{
	ssize_t	n;
	char	buf[MAXLINE];

again:
	while((n = read(sockfd, buf, MAXLINE)) > 0)
		write(sockfd, buf, n);

	if(n < 0 && errno == EINTR)
		goto again;
	else if(n < 0)
		printf("str_echo : read error");
}

int
main(int argc, char **argv)
{
	int	listenfd, connfd;
	pid_t	childpid;
	socklen_t	clilen;
	struct	sockaddr_in	cliaddr, servaddr;

	listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if(listenfd == -1){
		printf("socket fail.\n");
		return -1;
	}

	bzero(&servaddr, sizeof(struct sockaddr_in));
	servaddr.sin_family 	 = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port 		 = htons(SERV_PORT);

	if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))){
		printf("bind fail.\n");
		return -1;
	}
	if(listen(listenfd, 5)){
		printf("listen fail.\n");
		return -1;
	}

	printf("listen stat.\n");

	for(;;){
		clilen = sizeof(cliaddr);
		connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
		if(connfd == -1){
			printf("accept fail.\n");
			return -1;
		}
		printf("accept stat.\n");
		if((childpid = fork()) == 0){
			close(listenfd);
			printf("servers.\n");
			str_echo(connfd);
			exit(0);
		}
		close(connfd);
	}

	return 0;
}

【UNIX网络编程(四)】TCP套接字编程详细分析,布布扣,bubuko.com

时间: 2024-11-08 18:16:35

【UNIX网络编程(四)】TCP套接字编程详细分析的相关文章

UNIX网络编程笔记(3)—基本TCP套接字编程

基本TCP套接字编程 主要介绍一个完整的TCP客户/服务器程序需要的基本套接字函数. 1.概述 在整个TCP客户/服务程序中,用到的函数就那么几个,其整体框图如下: 2.socket函数 为了执行网络I/O,一个进程必须要做的事情就是调用socket函数.其函数声明如下: #include <sys/socket.h> int socket(int family ,int type, int protocol); 其中: family:指定协议族 type:指定套接字类型 protocol:指

Unix网络编程之基本TCP套接字编程(上)

TCP客户/服务器实例 服务器程序 #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); //1 bzero(&servaddr, sizeof(servad

【UNIX网络编程(二)】基本TCP套接字编程函数

基于TCP客户/服务器程序的套接字函数图如下: 执行网络I/O,一个进程必须做的第一件事就是调用socket函数,指定期望的通信协议类型. #include <sys/socket.h> int socket(int family, int type, int protocol);/*返回值:若成功则为非负描述符,若出错则为-1*/ socket函数成功时返回一个小的非负整数值,它与文件描述符类似,把它称为套接字描述符,简称sockfd.family参数指明协议族,被称为协议域.type参数指

《网络编程》基于 TCP 套接字编程的分析

本节围绕着基于 TCP 套接字编程实现的客户端和服务器进行分析,首先给出一个简单的客户端和服务器模式的基于 TCP 套接字的编程实现,然后针对实现过程中所出现的问题逐步解决.有关基于 TCP 套接字的编程过程可参考文章<基本 TCP 套接字编程>.该编程实现的功能如下: (1)客户端从标准输入读取文本,并发送给服务器: (2)服务器从网络输入读取该文本,并回射给客户端: (3)客户端从网络读取由服务器回射的文本,并通过标准输出回显到终端: 简单实现流图如下:注:画图过程通信双方是单独的箭头,只

《网络编程》基本 TCP 套接字编程

在进行套接字编程之前必须熟悉其地址结构,有关套接字的地址结构可参考文章<套接字编程简介>.基于 TCP 的套接字编程的所有客户端和服务器端都是从调用socket 开始,它返回一个套接字描述符.客户端随后调用connect 函数,服务器端则调用 bind.listen 和accept 函数.套接字通常使用标准的close 函数关闭,但是也可以使用 shutdown 函数关闭套接字.下面针对套接字编程实现过程中所调用的函数进程分析.以下是基于 TCP 套接字编程的流程图: socket 函数 套接

unix网络编程第四章----基于TCP套接字编程

为了执行网络I/O操作.进程必须做的第一件事情就是调用Socket函数.指定期待的通信协议 #include<sys/socket.h> int socket(int family,int type,int protocol); family表示协议族,比如AF_INET,type表示套接字类型, protocol一般设置为0 family: AF_INET ipv4协议 type: SOCK_STREAM 字节流套接字 SOCK_DGRAM 数据报套接字 SOCK_RAW 原始套接字 pro

Unix网络编程学习笔记之第4章 基于TCP套接字编程

1. socket函数 int socket(int family, int type,int protocol) 成返回一个套接字描述符.错误返回-1 其中family指定协议族,一般IPv4为AF_INET, IPv6为AF_INET6. 其中type指定套接字类型,字节流:SOCK_STREAM.   数据报:SOCK_DGRAM. 一般情况下通过family和type的组合都可以唯一确定一个套接字类型.所以一般我们就把protocol设为0就可以了. 有时在某些特殊情况下,family和

【Unix网络编程】chapter3 套接字编程简介

chapter3套接字编程简介3.1 概述 地址转换函数在地址的文本表达和他们存放在套接字地址结构中的二进制值之间进行转换.多数现存的IPv4代码使用inet_addr和inet_ntoa这两个函数,不过这两个新函数inet_pton和inet_ntop同时适用于IPv4和IPv6. 3.2 套接字地址结构 sockaddr_ 3.2.1 IPv4套接字地址结构 IPv4套接字地址结构通常也称为"网际套接字地址结构",它以sockaddr_in命令,定义在<netinet/in.

基本的TCP套接字编程

上图基本展示了TCP客户端与服务器编程的基本的流程. 1.面向连接编程(TCP) 面向连接的网络应用程序开发流程比较固定,需要开发者创建服务器与客户端两个应用程序,通过网络是想进程间的通讯. ●     服务器端流程 1        创建套接字(socket) 2        服务绑定(bind) 3        服务侦听(listen) 4        处理新到连接(accept) 5       数据收发(recv\send) 6       套接字关闭(close) ●     客