socket基本知识记录

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时发现的一篇比较好的文章,做的记录,部分错误已经做了修改。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值