基本的TCP套接字

基本的TCP套接字

IPv4 TCP客户

典型的TCP客户的通信涉及四个步骤:

  1. 使用socket()创建TCP套接字。
  2. 使用connect()建立到达服务器的连接。
  3. 使用send()和recv()通信。
  4. 使用close()关闭连接。

一. 应用程序建立与参数解析

(一)包括文件:声明了API的标准函数与常量。参考文档,了解用于系统上的套接字函数和数据结构的合适的包括文件。利用了自己的包括文件Practical.h,它带有用于自己函数的原型。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Practical.h"

(二)典型的参数解析和可靠性检查:作为前两个参数传入要发回的IPv4地址和字符串。客户可以选择接受服务器端口作为第三个参数。如果没有提供端口,客户将使用众所周知的应答协议端口。

int main(int argc, char *argv[]){

	if(argc < 3||argc > 4)//Test for correct number of arguments
		DieWithUserMessage("Parameter(s)","<Server Address><Echo Word>[<Server Port>]");

	char *servIP = argv[1];//First arg : server IP address(dotted quad)
	char *echoString = argv[2];// Second arg : string to echo
	//Third arg(optional):server port(numeric). 7 is well-konwn echo port
	in_port_t servPort = (argc = 4) ? atoi(argv[3]) : 7;
}

二. TCP套接字的创建

使用socket()函数创建套接字。该套接字用于IPv4(af_inet),使用称为TCP(ipproto_tcp)的基于流的协议(sock_stream)。如果成功,socket()就会为套接字返回一个整数值的描述符或者“句柄”。如果套接字失败,返回-1,并调用错误处理函数DieWithSystemMessage(),打印一个提示信息的提示符并退出。

	//Create a reliable, stream socket using TCP
	int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock < 0)
		DieWithSystemMessage("socket() failed");

三. 准备地址并建立连接:

  • 准备sockaddr_in结构用于存放服务器地址。调用memset()确保未显式设置的结构的任何部分都包含0.
  • 填充sockaddr_in:必须设置地址族(AF_INET)、Internet地址和端口号。函数inet_pton()把服务器的Internet地址的字符串表示(以点分四组表示法作为命令行参数传入)转换为32位的二进制表示。调用htons()(意思是“host to network short”)确保根据API需要对二进制值进行格式化。
  • 连接:connect()函数用于建立给定套接字与由sockaddr_in结构中的地址和端口标识的套接字之间的连接。由于Sockets API是通用的,需要把指向sockaddr_in地址结构的指针(它特定于IPv4地址)强制转换(cast)为泛型类型(sockaddr*),并且必须提供地址数据结构的实际大小。
	//Construct the server address structure
	struct sockaddr_in servAddr;//Server address
	memset(&servAddr, 0, sizeof(servAddr));//Zero out structure
	servAddr.sin_family = AF_INET;//IPv4 address family
	//Convert address
	int rtnVal = inet_pton(AF_INET, servIP, &servAddr.sin_addr.s_addr);
	if(rtnVal == 0)
		DieWithUserMessage("inet_pton() failed","invalid address string");
	else if(rtnVal < 0)
		DieWithSystemMessage("inet_pton() failed");
	servAddr.sin_port = htons(servPort);//Server port
	//Establish the connection to the echo server
	if(connect(sock,(struct  sockaddr *) &servAddr, sizeof(servAddr)) < 0)
		DieWithSystemMessage("connect() failed");

四.把应答字符串发送给服务器:

查明参数字符串的长度并保存它。把指向应答字符串的指针传递给send()调用。如果成功send返回发送的字节数,否则返回-1.

	size_t echoStringLen = strlen(echoString);//Determine input length

	//Send the string to the server
	ssize_t numBytes = send(sock, echoString, echoStringLen, 0);
	if(numBytes < 0)
		DieWithSystemMessage("send() failed");
	else if(numBytes != echoStringLen)
		DieWithUserMessage("send()", "sent unexpected number of bytes");

五.接受应答服务器的答复

TCP是一种字节流协议。这类协议的实现不会保持send()的边界。通过在连接的一端调用send()发送的字节可能不会通过在另一端单独调用一次recv()而全都返回。因此需要反复接受字节,直到接收到的字节数与发送的字节数相等为止。编写使用套接字的应用程序的基本原则是:对于另一端的网络和程序将要做什么事情,永远都不能作出假设

  • 接受字节块:recv()会阻塞到数据可用为止,并且返回复制到缓冲区的字节数,如果失败则返回-1.如果返回值为0,则指示另一端的应用程序关闭了TCP连接。注意:传递给recv()的大小参数将为添加null终止字符预留空间。
  • 打印缓冲区:在接受到服务器发送的数据时将打印它。在接受到每个数据块末尾添加null终止符(0),以便fputs()可以把它作为字符串处理。
  • 打印换行符:当接收到与发送一样多的字节时,退出循环,打印一个换行符。
//Recieve the same string back from the server
	unsigned int totalBytesRcvd = 0;//Count of total bytes received
	fputs("Received: ", stdout);//Setup to print the echoed string
	while(totalBytesRcvd < echoStringLen){
		char buffer[BUFSIZE]; //I/O buffer
		//Receive up to the buffer size(minus 1 to leave space for a null terminator) bytes from the sender
		numBytes = recv(sock, buffer, BUFSIZE - 1, 0);
		if(numBytes < 0)
			DieWithSystemMessage("recv() failed");
		else if(numBytes == 0)
			DieWithUserMessage("recv()", "connection closed prematurely");
		totalBytesRcvd += numBytes;//Keep tally of total bytes
		buffer[numBytes] = '\0';//Terminate the string
		fputs(buffer, stdout);//Print the echo buffer
	}

	fputc('\n',stdout);//Print a final linefeed

六.终止连接并退出

close()函数通知远程套接字通信结束,然后释放分配给套接字的本地资源。

	close(sock);
	exit(0);

###错误处理函数
客户应用程序利用了如下两个错误处理函数

DieWithUserMessage(const char *msg, const char *detail)
DieWithSystemMessage(const char *msg)

这两个函数把用户提供的消息字符串(msg)打印到stderr,其后接着是详细的消息字符串;然后,它们利用一个错误返回代码调用exit(),导致应用程序终止。区别是详细消息的来源,对于DieWithUserMessage(),详细信息是用户提供的,对于DieWithSystemMessage(),消息是系统基于错误变量errno的值提供的。仅当错误情况是由设置errno的系统调用产生的时,才会调用DieWithSystemMessage()。

尽量避免使用printf()输出固定的、预先格式化的字符串。你永远也不应该做的一件事是:把网络接受到的文本作为第一个参数传递给printf()。它会引起严重的安全性问题,要代之以使用fputs()。

#include<stdio.h>
#include<stdlib.h>

void DieWithUserMessage(const char *msg, const char *detail){
	fputs(msg, stderr);
	fputs(": ", stderr);
	fputs(detail, stderr);
	fputc('\n',stderr);
	exit(1);
}

void DieWithSystemMessage(const char *msg){
	perror(msg);
	exit(1);
}

IPv4 TCP服务器

基本的TCP服务器通信,要执行以下步骤:

  1. 使用socket()创建TCP套接字。
  2. 利用bind()给套接字分配端口号。
  3. 使用listen()告诉系统允许对该端口建立连接。
  4. 反复执行以下操作
    4.1 调用accept()为每个客户连接获取新的套接字。
    4.2 使用send()和recv()通过新的套接字与客户通信。
    4.3 使用close()关闭客户连接。

程序建立和参数解析

使用atoi()把端口号从字符转换为数值;如果第一个参数不是一个数字,atoi()将返回0,以后当调用bind()会引发一个错误。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Practical.h"

static const int MAXPENDING = 5;//Maximum outstanding connection requests

int main(int argc, char *argv[])
{
	if(argc != 2)// Test for cprrect number of arguments
		DieWithUserMessage("Parameter(s)", "<Server Port>");

	in_port_t servPort = atoi(argv[1]);//first arg : local port

套接字的创建和设置

  • 创建一个流套接字

  • 填充想要的端点地址:

    • 在服务器上,需要把服务器套接字与地址和端口号相关联,使得客户连接可以到达正确的位置。

    • 由于为IPv4编写程序,我们使用sockaddr_in结构。

    • 允许系统通过指定通配符地址inaddr_any作为我们想要的Internet地址来选择它。

    • 在sockaddr_in设置前,分别使用htonl()和htons()把它们转换为网络字节顺序(byte order)。

  • 把套接字绑定到指定的地址和端口:

    • 服务器的套接字需要与本地地址和端口相关联,bind()函数用来完成这个任务

    • 客户必须给connect()提供服务器的地址,而服务器必须给bind()指定它自己的地址。它们必须对这份信息(副武器的地址和端口)达成协议以便进行通信,它们实际都不知道客户的地址

  • 设置要侦听的套接字:listen()调用告诉TCP实现允许来自客户的进入的连接。调用listen()前所有的连接请求都被拒绝。

//Create socket for incoming connections
	int servSock;//socket description for server
	if((servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		DieWithSystemMessage("socket() failed");

	//Construct local address structure
	struct sockaddr_in servAddr;//Local address
	memset(&servAddr, 0, sizeof(servAddr));//Zero out structure
	servAddr.sin_famliy = AF_INET;//IPv4 address family
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);//Any incoming interface
	servAddr.sin_port = htonl(servPort);//Local port

	//Bind to the local address
	if(bind(servSock, (struct sockaddr*) &servAddr, sizeof(servAddr)) < 0)
		DieWithSystemMessage("bind() failed");
	
	//Mark the socket so it will listen for incoming connections
	if (listen(servSock, MAXPENDING) < 0)
		DieWithSystemMessage("listen() failed");

反复处理进入的连接

  • 接受进入的连接

    • 在其上调用listen()的TCP套接字使用方式不同于在客户应用程序看见的套接字,服务器应用程序不是在套接字上执行发送和接受,而是调用accept(),它会阻塞直到建立了连接以侦听套接字的端口号为止,此时accept()为新套接字返回一个描述符。

    • 第二个参数指向sockaddr_in结构,第三个参数是一个指针,指向此结构的长度。

    • 一旦成功,sockaddr_in就会包含返回的套接字连接到的客户的Internet地址和端口,并将这个地址的长度写入第三个参数指向的整数

  • 报告被连接客户

    • 此时clntAddr包含连接客户的地址和端口号。

    • 我们提供了一个“呼叫者ID”的函数,并打印客户的信息。

    • inet_ntop()执行与inet_pton()相反的操作,接受客户地址的二进制表示。并转化为点分四组格式的字符串。

    • 由于实现以所谓的网络字节顺序处理端口和地址,在把端口号传递给pritf()前必须转换它

  • 处理应答客户:HandleTCPClient负责“应用程序协议”

for(;;){//Run forever
		struct sockaddr_in clntAddr;//Client address
		//Set length of client address structure(in-out parameter)
		socklen_t clntAddrLen = sizeof(clntAddr);

		//Wait for a client to connect
		int clntSock = accept(servSock, (struct sockaddr*)&clntAddr, &clntAddrLen);
		if(clntSock < 0)
			DieWithSystemMessage("accept() failed");

		//clntSock is connected to a client
		char clntName[INET_ADDRSTRLEN];//String to contain client address
		if(inet_ntop(AF_INET, &clntAddr.sin_addr.s_addr, clntName, sizeof(clntName)) != NULL)
			printf("Handling client %s/%d\n", clntName, ntohs(clntAddr.sin_port));
		else
			puts("Unable to get client address");

		HandleTCPClient(clntSock);
	}

分离服务器的特定于“应答”的部分

  • 促进代码重用

  • HandleTCPClient()在给定的套接字上接受数据,并在相同套接字上发回它,只要recv()返回一个正值(指示收到数据),这个过程就会反复进行。recv()会阻塞到接受到数据或者客户端关闭连接为止。当客户正常的关闭连接时,recv()返回0.

for(;;){//Run forever
		struct sockaddr_in clntAddr;//Client address
		//Set length of client address structure(in-out parameter)
		socklen_t clntAddrLen = sizeof(clntAddr);

		//Wait for a client to connect
		int clntSock = accept(servSock, (struct sockaddr*)&clntAddr, &clntAddrLen);
		if(clntSock < 0)
			DieWithSystemMessage("accept() failed");

		//clntSock is connected to a client
		char clntName[INET_ADDRSTRLEN];//String to contain client address
		if(inet_ntop(AF_INET, &clntAddr.sin_addr.s_addr, clntName, sizeof(clntName)) != NULL)
			printf("Handling client %s/%d\n", clntName, ntohs(clntAddr.sin_port));
		else
			puts("Unable to get client address");

		HandleTCPClient(clntSock);
	}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值