基本的TCP套接字
IPv4 TCP客户
典型的TCP客户的通信涉及四个步骤:
- 使用socket()创建TCP套接字。
- 使用connect()建立到达服务器的连接。
- 使用send()和recv()通信。
- 使用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服务器通信,要执行以下步骤:
- 使用socket()创建TCP套接字。
- 利用bind()给套接字分配端口号。
- 使用listen()告诉系统允许对该端口建立连接。
- 反复执行以下操作
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);
}