1.socket建模
socket的初衷是一个庞大的体系,TCP/IP只是这个庞大体系下一个很小的子集,而我们真正能用上的更是这个子集中的一小部分:运输层(Host-to-Host Transport Layer)的TCP和UDP协议,以及使用这两个协议进行应用层(Application Layer)的开发。
即使是socket的核心部分,网络层(Internet Layer)的IP协议,在编程的时候我们也很少会感觉到它的存在——因为已经被封装好了,我们唯一需要做的事情就是传入一个宏。
数据传输的顺序是:
链路层--网络层--运输层--应用层
建模代码:
class TcpServer
{
private :
int listenSock;
int communicationSock;
sockaddr_in servAddr;
sockaddr_in clntAddr;
public :
TcpServer( int listen_port);
bool isAccept();
void handleEcho();
};
sock实际上就是socket,addr就是address。serv和clnt我想你一定能猜到是server和client吧。还有一个 socket中的结构体sockaddr_in,实际上就是这个意思:socket address internet(网络嵌套字地址)。
2.socket与文件描述符
UNIX中一切都是文件。
#include<unistd.h>
常用的3个已经打开的fd,0:标准输入(STDIN_FILENO);1:标准输出(STDOUT_FILENO);2:标准错误(STDERR_FILENO)。
注意:file和fd并非一定是一一对应的。当一个file被多个程序调用的时候,会生成相互独立的fd。
文件是应用程序与系统(包括特定硬件设备)之间的桥梁,而文件描述符就是应用程序使用这个“桥梁”的接口。在需要的时候,应用程序会向系统申请一个文件,然后将文件的描述符返回供程序使用。返回socket的文件通常被创建在/tmp或者/usr/tmp中。
3.sockaddr和sockaddr_in
socket的通用address描述结构sockaddr:
struct sockaddr
{
unsigned short sa_family;
char sa_data[ 14 ];
};
这是一个16字节大小的结构(2+14)(不考虑内存对齐?),sa_family可以认为是socket address family的缩写,也可能被简写成AF(Address Family),当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。
我们实际在构造sockaddr时候用到的结构sockaddr_in(意指socket address internet):
struct sockaddr_in
{
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[ 8 ];
};
sin_addr被定义成了一个结构,这个结构实际上就是:
struct in_addr
{
unsigned long s_addr;
};
头文件依赖关系:
#include < sys / socket.h >
#include < arpa / inet.h >
ARPA是 Advanced research project agency(美国国防部高级研究计划暑)的所写,ARPANET是当今互联网的前身,所以我们就可以想象,为什么inet.h会在arpa目录下了。
4.构造函数以及涉及的概念
完整的头文件代码:
// Filename: TcpServerClass.hpp
#ifndef TCPSERVERCLASS_HPP_INCLUDED
#define TCPSERVERCLASS_HPP_INCLUDED
#include < unistd.h >
#include < iostream >
#include < sys / socket.h >
#include < arpa / inet.h >
class TcpServer
{
private :
int listenSock;
int communicationSock;
sockaddr_in servAddr;
sockaddr_in clntAddr;
public :
TcpServer( int listen_port);
bool isAccept();
void handleEcho();
};
#endif // TCPSERVERCLASS_HPP_INCLUDED
现在来写这个类的构造函数:
TcpServer::TcpServer( int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) {
throw " socket() failed " ;
}
memset( & servAddr, 0 , sizeof (servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(listen_port);
if ( bind(listenSock, (sockaddr * ) & servAddr, sizeof (servAddr)) < 0 ) {
throw " bind() failed " ;
}
if ( listen(listenSock, 10 ) < 0 ) {
throw " listen() failed " ;
}
}
网络分层:链路——网络——传输——应用。
数据从应用程序里诞生,传送到互联网上每一层都会进行一次封装:
Data>>Application>>TCP/UDP>>IP>>OS(Driver, Kernel & Physical Address)。
socket重点描述的是协议,包括网络协议(IP)和传输协议(TCP/UDP)。
sockaddr重点描述的是地址,包括IP地址和TCP/UDP端口。
5.socket()函数:
socket()的函数原型是:
int socket( int protocolFamily, int type, int protocol);
第一个参数是协议簇(Linux里面叫作域,意思一样的);
第二个参数是传输层协议类型,我们教程里用到的宏,只有两个:SOCK_STREAM(数据流格式--TCP)和SOCK_DGRAM(数据报格式--UDP);
第三个参数是具体的传输层协议。当赋值为0的时候,系统会根据传输层协议类型自动匹配和选择。
6.数据的地址
数据的传送是通过socket进行的。但是socket只描述了协议类型。要让数据正确的传送到某个地方,必须添加那个地方的sockaddr地址;同样,要能接受网络上的数据,必须有自己的sockaddr地址。
在网络上传送的数据包,是socket和sockaddr共同“染指”的结果。他们共同封装和指定了一个数据包的网络协议(IP)和IP地址,传输协议(TCP/UDP)和端口号。
7.网络字节和本机字节的转换
sockaddr结构中的IP地址(sin_addr.s_addr)和端口号(sin_port)将被封装到网络上传送的数据包中,所以,它的结构形式需要保证是网络字节形式。这里用到的函数是htons()和htonl(),这些缩写的意思是:
h: host,主机(本机)
n: network,网络
to: to转换
s: short,16位(2字节,常用于端口号)
l: long, 32位(4字节,常用于IP地址)
“反过来”的函数也是存在的ntohs()和ntohl()。
8.创建监听套接字
我们首先通过socket()系统调用创建了listenSock,然后通过为结构体赋值的方法具体定义了服务器端的sockaddr。
需要补充的是说明宏定义INADDR_ANY。这里的意思是使用本机所有可用的IP地址。当然,如果你机器绑定了多个IP地址,你也可以指定使用哪一个。
在实际使用的时候,一个socket至少会绑定一个本机的sockaddr,没有自己的“地址信息”,就不能接受到网络上的数据包(至少在TCP协议里面是这样的)。
9.socket和本机sockaddr的绑定
bind()函数原型:
int bind( int socket, struct sockaddr * localAddress, unsigned int addressLength);
10.监听
listen socket的目的是准备被动的接受来自“所有”sockaddr的请求:
int listen( int socket, int queueLimit);
其中第二个参数是等待队列的限制,一般设置在5-20。
11.创建通讯套接字
实际上所有的socket都有通讯的功能,只是在我们的例子中,之前那个socket只负责listen,而这个socket负责接受信息并echo回去。
我们现看看这个函数:
bool TcpServer::isAccept()
{
unsigned int clntAddrLen = sizeof (clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr * ) & clntAddr, & clntAddrLen)) < 0 ) {
return false ;
} else {
std::cout << " Client(IP: " << inet_ntoa(clntAddr.sin_addr) << " ) connected./n " ;
return true ;
}
}
用accept()函数创建新的socket:
communicationSock实际上是用函数accept()创建的。
int accept( int socket, struct sockaddr * clientAddress, unsigned int * addressLength);
在socket创建后,会将客户端sockaddr的数据以及结构体的大小传回。
当程序调用accept()的时候,程序有可能就停下来等accept()的结果。这就是我们前一小节说到的block(阻塞)。这如同我们调用 std::cin的时候系统会等待输入直到回车一样。accept()是一个有可能引起block的函数。请注意我说的是“有可能”,这是因为 accept()的block与否实际上决定与第一个参数socket的属性。这个文件描述符如果是block的,accept()就block,否则就 不block。默认情况下,socket的属性是“可读可写”,并且,是阻塞的。所以,我们不修改socket属性的时候,accept()是阻塞的。
accept()只是在server端被动的等待,它所响应的,是client端connect()函数:
int connect( int socket, struct sockaddr * foreignAddress, unsigned int addressLength);
12.新socket和原先socket的联系
accept()创建的新socket(我们例子中的communicationSock,这里我们简单用newSock来带指)首先包含了listen socket的信息,所以,newSock具有本机sockaddr的信息;其次,因为它响应于client端connect()函数的请求,所以,它还 包含了clinet端sockaddr的信息。
inet_ntoa()函数:
extern char * inet_ntoa ( struct in_addr __in) __THROW;
对于这个函数,我们可以作为一种,将IP地址,由in_addr结构转换为可读的ASCII形式的固定用法。
13.TCP通讯模型
TCP的Server/Client模型类似这样:
ServApp——ServSock——Internet——ClntSock——ClntApp
TCP的server端至少有两个socket,一个用于监听,一个用于通讯;TCP的client端可以只有一个socket,这个socket同时“插”在server的两个socket上。当然,插上listen socket的目的只是为了创建communication socket,创建完备后,listen是可以关闭的。但是,如果这样,其他的client就无法再连接上server了。
recv()和send():
int send( int socket, const void * msg, unsigned int msgLength, int flags);
int recv( int socket, void * rcvBuffer, unsigned int bufferLength, int flags);
这两个函数的第一个参数是用于“通讯”的socket,第二个参数是发送或者接收数据的起始点指针,第三个参数是数据长度,第四个参数是控制符号(默认属性设置为0就可以了)。失败时候传回-1,否则传回实际发送或者接收数据的大小,返回0往往意味着连接断开了。
14.关于close()函数
extern int close ( int __fd);
这个函数用于关闭一个文件描述符,自然,也就可以用于关闭socket。
关于这篇文章的所有代码:
// Filename: TcpServerClass.hpp
#ifndef TCPSERVERCLASS_HPP_INCLUDED
#define TCPSERVERCLASS_HPP_INCLUDED
#include < unistd.h >
#include < iostream >
#include < sys / socket.h >
#include < arpa / inet.h >
#include <stdlib.h>
#include<string.h>
class TcpServer
{
private :
int listenSock;
int communicationSock;
sockaddr_in servAddr;
sockaddr_in clntAddr;
public :
TcpServer( int listen_port);
bool isAccept();
void handleEcho();
};
#endif // TCPSERVERCLASS_HPP_INCLUDED
// Filename: TcpServerClass.cpp
#include " TcpServerClass.hpp "
TcpServer::TcpServer( int listen_port)
{
if ( (listenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ) {
throw " socket() failed " ;
}
memset( & servAddr, 0 , sizeof (servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(listen_port);
if ( bind(listenSock, (sockaddr * ) & servAddr, sizeof (servAddr)) < 0 ) {
throw " bind() failed " ;
}
if ( listen(listenSock, 10 ) < 0 ) {
throw " listen() failed " ;
}
}
bool TcpServer::isAccept()
{
unsigned int clntAddrLen = sizeof (clntAddr);
if ( (communicationSock = accept(listenSock, (sockaddr * ) & clntAddr, & clntAddrLen)) < 0 ) {
return false ;
} else {
std::cout << " Client(IP: " << inet_ntoa(clntAddr.sin_addr) << " ) connected./n " ;
return true ;
}
}
void TcpServer::handleEcho()
{
const int BUFFERSIZE = 32 ;
char buffer[BUFFERSIZE];
int recvMsgSize;
bool goon = true ;
while ( goon == true ) {
if ( (recvMsgSize = recv(communicationSock, buffer, BUFFERSIZE, 0 )) < 0 ) {
throw " recv() failed " ;
} else if ( recvMsgSize == 0 ) {
goon = false ;
} else {
if ( send(communicationSock, buffer, recvMsgSize, 0 ) != recvMsgSize ) {
throw " send() failed " ;
}
}
}
close(communicationSock);
}
// Filename: main.cpp
// Tcp Server C++ style, single work
#include < iostream >
#include " TcpServerClass.hpp "
int echo_server( int argc, char * argv[]);
int main( int argc, char * argv[])
{
int mainRtn = 0 ;
try {
mainRtn = echo_server(argc, argv);
}
catch ( const char * s ) {
perror(s);
exit(EXIT_FAILURE);
}
return mainRtn;
}
int echo_server( int argc, char * argv[])
{
int port;
if ( argc == 2 ) {
port = atoi(argv[ 1 ]);
} else {
port = 5000 ;
}
TcpServer myServ(port);
while ( true ) {
if ( myServ.isAccept() == true ) {
myServ.handleEcho();
}
}
return 0 ;
}
声明:本文是我在学习socket时发现的一篇比较好的文章,做的记录,部分错误已经做了修改。