【Linux】网络编程套接字

一.UDP套接字
UDP通信流程
客户端
1.创建套接字
2.为套接字绑定地址(客户端不推荐主动绑定)
3.发送数据
4.接收数据
5.关闭套接字

#include"udpsocket.hpp"                                                                                    
#include<sstream>
 
using namespace std;

int main(int argc,char* argv[])
{
   if(argc!=3)
   {
     cerr<<"./udp_cli ip port"<<endl;
     return -1;
   }
   uint16_t port;
   string ip=argv[1];
   stringstream tmp;
   tmp<<argv[2];
   tmp>>port;
   UdpSocket sock;
   CHECK_RET(sock.Socket());
 //客户端不推荐用户主动绑定,因为一个端口只能被一个进程占用,一旦固定端口,这个客户端只能启动一份
   while(1)
   {
     string buf;
     cin>>buf;
     //当socket还未绑定地址,这时操作系统在发送之前可以检测到,
     //操作系统会为socket选择一个合适的地址和端口进行绑定
     sock.Send(buf,ip,port);
     buf.clear();
     sock.Recv(buf,ip,port);
     cout<<"server say:"<<buf<<endl;
   }
   sock.Close();
   return 0;
 }

服务端
1.创建套接字
2.为套接字绑定地址
3.接收数据
4.发送数据
5.关闭套接字

#include"udpsocket.hpp"                                                                                    
#include<sstream>
 
using namespace std;

int main(int argc,char *argv[])
    //argc:运行参数个数
{
  if(argc!=3)
  {
    cerr<<"./udp_srv 192.168.216.128 9000"<<endl;
    return -1;
  }
  uint16_t port;
  string ip=argv[1];
  stringstream tmp;
  tmp<<argv[2];
  tmp>>port;
  UdpSocket sock;
  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind(ip,port));
  while(1)
  {
    string buf;
    string peer_ip;
    uint16_t peer_port;
    sock.Recv(buf,peer_ip,peer_port);
    cout<<"client-["<<peer_ip<<":"<<peer_port<<"]say:"<<buf<<endl;
    buf.clear();
    cin>>buf;
    sock.Send(buf,peer_ip,peer_port);
  }
  sock.Close();
 }

udpsocket.hpp

//封装udpsocket类,实例化对象,向外提供简单的socket接口                                                    
//1.创建套接字
//2.绑定地址信息
//3.发送数据
//4.接收数据
//5.关闭套接字

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#define CHECK_RET(q) if((q)==false){return -1;}
//检测返回值

using namespace std;

class UdpSocket
{
private:
  int _sockfd;
public:
  bool Socket()
  {
    _sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
    //AF_INET:ipv4
    //SOCK_DGRAM:数据报套接字
    //IPPROTO_UDP:传输层协议
    if(_sockfd<0)
    {
      cerr<<"socket error"<<endl;
      return false;
    }
    return true;
  }
  bool Bind(const string& ip,const uint16_t port)
  {
    struct sockaddr_in addr;
    //ipv4地址域使用sockaddr_in
    addr.sin_family=AF_INET;
    //ipv4地址域
    addr.sin_port=htons(9000);
    //host to net short
    //将主机字节序的16位数据,转换为网络字节序数据返回
    addr.sin_addr.s_addr=inet_addr(ip.c_str());
    //将点分十进制字符串IP地址转换为网络字节序IP地址
    int ret;
    socklen_t len=sizeof(struct sockaddr_in);
    ret=bind(_sockfd,(struct sockaddr*)&addr,len);
    if(ret<0)
    {
      cerr<<"bind error"<<endl;
      return false;
    }
    return true;
  }
  bool Send(const string& data,const string& peer_ip,const uint16_t peer_port)
      //peer:对端
  {
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(peer_port);
    addr.sin_addr.s_addr=inet_addr(peer_ip.c_str());
    socklen_t len=sizeof(struct sockaddr_in);
    int ret=sendto(_sockfd,&data[0],data.size(),0,(struct sockaddr*)&addr,len);
    if(ret<0)
    {
      cerr<<"sendto error"<<endl;
      return false;
    }
    return true;
  }
  bool Recv(string& buf,string& peer_ip,uint16_t& peer_port)                                               
  //成功返回实际接收的数据长度,失败返回-1
  {
    struct sockaddr_in peer_addr;
    socklen_t len=sizeof(struct sockaddr_in);
    char tmp[4096]={0};
    int ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&peer_addr,&len);
    if(ret<0)
    {
      cerr<<"recv error"<<endl;
      return false;
    }
    peer_ip=inet_ntoa(peer_addr.sin_addr);
    //将网络字节序IP地址转换为点分十进制字符串IP地址
    peer_port=ntohs(peer_addr.sin_port);
    //将网络字节序的16位数据转换为主机字节序数据
    buf.assign(tmp,ret);
    //从tmp中拷贝ret长度的数据到buf中
    return true;
  }
  void Close()
  {
    close(_sockfd);
  }
};

二.TCP套接字
TCP通信流程
客户端
1.创建套接字
2.为套接字绑定地址(不推荐主动绑定)
3.向服务端发起连接
4.发送数据
5.接收数据
6.关闭套接字

/tcp客户端通信流程                                                                                        
//1.创建套接字
//2.为套接字绑定地址信息(不推荐童虎主动绑定)
//3.向服务端发起连接请求
//4.发送数据
//5.接收数据
//6.关闭套接字
#include"tcpsocket.hpp"
#include<iostream>
#include<signal.h>
using namespace std;

void sigcb(int signo)
{   
    printf("recv a signo SIGPIPE---connect shutdoen\n");
}
int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        cerr<<"./tcp_cli ip port"<<endl;
        return -1;
    }
    signal(SIGPIPE,sigcb);
    TcpSocket sock;
    CHECK_RET(sock.Socket());
    CHECK_RET(sock.Connect(argv[1],argv[2]));
    while(1)
    {
        string buf;
        cout<<"client say:";
        fflush(stdout);
        cin>>buf;
        sock.Send(buf);
        buf.clear();
        sock.Recv(buf);
        cout<<"server say:"<<buf<<endl;
    }
    sock.Close();
    return 0;
}

服务端
1.创建套接字
2.为套接字绑定地址
3.开始监听
4.获取已完成连接
5.通过获取的已完成连接socket接收数据
6.通过获取的已完成连接socket发送数据
7.关闭套接字

//tcp服务端通信流程                                                                                        
//1.创建套接字
//2.为套接字绑定地址信息
//3.开始监听
//4.获取已完成连接socket
//5.通过获取的新建socket与客户端进行通信-接收数据
//5.发送数据
//7.关闭套接字
#include "tcpsocket.hpp"
#include<stdio.h>
using namespace std;

int main(int argc,char* argv[])
{
    if(argc!=3)
    {   
        cerr<<"./tcp_srv ip port"<<endl;
        return -1;
    }
    TcpSocket lst_sock;
    CHECK_RET(lst_sock.Socket());
    CHECK_RET(lst_sock.Bind(argv[1],argv[2]));
    CHECK_RET(lst_sock.Listen());
    while(1)
    {
        TcpSocket clisock;
        bool ret=lst_sock.Accept(clisock);
        if(ret==false)
        {
            continue;
        }
        string buf;
        ret=clisock.Recv(buf);
        if(ret==false)
        {
            clisock.Close();
            continue;
        }
        cout<<"client say:"<<buf<<endl;
        buf.clear();
        cout<<"server say:";
        fflush(stdout);
        cin>>buf;
        clisock.Send(buf);
    }
    lst_sock.Close();
    return 0;
}

tcpsocket.hpp

//封装一个tcpsocket类,向外提供简单的套接字接口
//1.创建套接字
//2.为套接字绑定地址信息
//3.开始监听
//4.向服务器端发起连接请求
//5.服务端获取新建连接
//6.发送数据
//7.接收数据
//8.关闭套接字

#include<iostream>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>                                                                                     
#include<string>
#include<sstream>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>

using namespace std;

#define CHECK_RET(q) if((q)==false){return -1;}

class TcpSocket
{
private:
   int _sockfd;
public:
    void SetFd(int fd)
    {
        _sockfd=fd;
    }
    int GetFd()
    {
        return _sockfd;
        }
    bool Socket()
    {
        _sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
        if(_sockfd<0)
        {
           cerr<<"socket error"<<endl;
            return false;
        }
        return true;
    }
    int str2int(const string& str)
    {   
        int num;
        stringstream tmp;
        tmp<<str;
        tmp>>num;
        return num;
    }
   bool Bind(const string& ip,const string& port)
    {   
        struct sockaddr_in addr;
        addr.sin_family=AF_INET;
        addr.sin_port=htons(str2int(port));
        addr.sin_addr.s_addr=inet_addr(ip.c_str());
        socklen_t len=sizeof(struct sockaddr_in);
        int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
        if(ret<0)
        {
            cerr<<"bind error"<<endl;
            return false;
        }
        return true;
     }
     bool Listen(const int backlog=5)
        //开始监听:通知操作系统,可以开始接收客户端的连接请求
        //并且完成三次握手建立连接过程
        //tcp的面向连接,有一个三次握手建立连接过程
        //backlog:客户端最大并发连接数(同一时间最多接收多少个客户端新连接请求)
     {   
        int ret=listen(_sockfd,backlog);
        if(ret<0)
        {
            cerr<<"listen error"<<endl;
            return false;
        }
        return true;
    }       
    bool Connect(const string& srv_ip,const string& srv_port)
    {   
        struct sockaddr_in addr;
        addr.sin_family=AF_INET;
        addr.sin_port=htons(str2int(srv_port));
        addr.sin_addr.s_addr=inet_addr(srv_ip.c_str());
        socklen_t len=sizeof(struct sockaddr_in);
        int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
        if(ret<0)
        {
            cerr<<"connect error"<<endl;
            return false;
        }   
        return true;
    }   
    bool Accept(TcpSocket &clisock,string *ip=NULL,uint16_t *port=NULL)
        //sockfd:监听套接字描述符
        //addr:客户端地址信息 
        //addrlen:地址信息长度
        //返回值:返回新建连接的socket描述符,用于与客户端进行数据通信
    {   
        struct sockaddr_in cliaddr;
        socklen_t len=sizeof(struct sockaddr_in);
        int newfd=accept(_sockfd,(sockaddr*)&cliaddr,&len);
        if(newfd<0)
        {
            cerr<<"accept error"<<endl;
            return false;
        }   
        clisock.SetFd(newfd);
        if(ip!=NULL)
        {
            *ip=inet_ntoa(cliaddr.sin_addr);
        }
        if(port!=NULL)
        {
            *port=ntohs(cliaddr.sin_port);
        }
        return true;
    }   
    bool Send(string& data)
        //sockfd:套接字你描述符(服务端是新建连接的socket描述符)
        //buf:要发送的数据
        //len:要发送的数据长度
        //flags:0-默认阻塞发送
        //返回值:成功返回实际发送的数据长度,失败返回-1
    {   
        int ret=send(_sockfd,&data[0],data.size(),0);
        if(ret<0)
        {   
            cerr<<"send error"<<endl;
            return false;
        }
        return true;
        }
    bool Recv(string& buf)
        //flags:0-默认阻塞接收
        //MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
        //返回值:>0返回实际接受的数据长度;=0连接断开;<0错误
    {
        char tmp[4096];
        int ret=recv(_sockfd,tmp,4096,0);
        if(ret<0)
        {
            cerr<<"recv error"<<endl;
            return false;
        }
        else if(ret==0)
        {
            cerr<<"connect shutdown"<<endl;
            return false;
        }
        buf.assign(tmp,ret);
        return true;
    }
    bool Close()
    {
        close(_sockfd);
    }
};

三.TCP协议
面向连接,可靠传输,面向字节流
协议字段信息
16位源/目的端口:实现端与端之间的数据传输
32位序号/确认序号:保证tcp数据的有序交付(包序管理)
4位首部长度:tcp报头长度(不包含数据),以4字节位单位,tcp报头大小范围:20~60字节
6位标志位:URG/ACK/PSH/RST/SYN/FIN
16位窗口大小:用于实现窗口滑动机制
16位校验和:校验接收的数据与发送的数据是否一致
16位紧急指针:带外数据
40字节选项管理:用到的时候才会有,意味着tcp报头长度不固定

面向连接:tcp的连接管理,三次握手,四次挥手

可靠传输
1.连接管理
2.确认应答机制
3.超时重传机制
4.协议字段中的序号/确认序号
5.协议字段中的校验和
可靠传输牺牲了部分性能,但有些损失是可以避免的
1.滑动窗口机制
通信双方在通信时会通过协议字段中的窗口字段协商窗口大小,告诉对方一次可以发送的最大数据量
通信双方通常在三次握手阶段,还会协商一个数据MSS(最大数据段大小–数据报中数据的最大长度)
发送方在发送数据的时候会将窗口大小的数据分成多个大小不大于MSS大小的数据报进行发送
1)流量控制
通过协议字段中的窗口字段,通知发送方能够发送的最大数据量,通过这个来限制对方的发送速度,避免发送过快,导致接收缓冲区塞满,而引起的后续数据丢包重传
2)快速重传
当接收方接收第二条数据,但是没有接收到第一条,则认为第一条有可能丢失,则立即向对方发送第一条数据的重传请求,并且将这个重传请求连续发送三次
发送方连续三次接收到重传请求,则对这条数据进行重传
连续发送三次重传请求,是为了避免有可能因为网络阻塞而接收到延迟的数据
(1)停等协议 (2)退n步协议 (3)选择重传协议
3)拥塞控制
发送方维护一个拥塞窗口,控制一次发送的数据量,以慢启动快增长的形式控制传输的数据量,起到对网络进行探测的作用,可以避免因为网络状况不好而导致的大量丢包

2.捎带应答机制
接收方为每一条接收到的数据组织报文,通过报文头部中的确认序号字段进行确认回复,这时如果刚好有要给对方发送的数据,则将这次的确认回复序号直接放到要发送的这条数据头中,可以节省一条空报文的回复,提高传输效率

3.延迟应答机制
接收方接收到数据之后,如果立即进行回复,窗口大小就会降低,导致传输吞吐率降低,降低了发送速度
这时如果接收数据之后,延迟一会进行确认回复,则又可能用户将缓冲区中的数据取走,保证传输吞吐率

提高性能
滑动窗口机制(流量控制,快速重传,拥塞控制)
延迟应答机制
捎带应答机制

字节流服务
send发送的数据,会先放到socket的发送缓冲区中,然后操作系统选择合适的实际将数据发送出去
多条小数据融合成一个大包发送出去,可以提高一定的传输性能
并且接收方,传输层数据的交付也比较灵活,可以一点一点交付,也可以一次性交付所有数据
但是字节流服务会造成数据的粘包问题:多条数据在缓冲区中粘连在一起
粘包的本质原因:tcp在传输层对数据的格式边界不敏感,不会替用户区分哪条数据从哪开始,到哪结束,只关注需要向用户交付多长字节的数据
因此粘包问题的解决方案就是用户在应用层进行数据的边界管理
1.特殊字符 2.数据定长 3.不定长数据在应用头中声明数据长度

tcp连接管理中保活机制
tcp通信双方若长时间无数据往来,则每隔一段时间向对方发送保活探测数据报,要求对方进行回复,若连续多条保活请求没有响应,则认为连接断开
连接断开在程序中的体现:recv读完所有数据之后,返回0,send触发异常SIGPIPE

传输层基于tcp实现的应用层协议:HTTP/FTP

四.UDP协议

无连接,不可靠,面向数据报
字段信息:16位源端口,16位目的端口,16位数据报长度,16位校验和
16位源/目的端口:实现端与端之间的数据传输
16位校验和:校验接收的数据与发送的数据是否一致(二进制反码求和算法)
16位数据报长度:udp数据报的总长度(包含udp头部信息在内)
无连接,不可靠:双方通信,不需要建立连接,只需要直到对方的地址信息就能发送数据
面向数据报:传输层向应用层交付数据的时候只能一整条一整条的交付
16位数据报长度,标识了一个完整的udp数据有多长,接收的收为了避免接收半条数据,导致缓冲区中的数据长度无法标识,导致交付混乱,因此,udp数据只能整条交付
决定了udp数据报最大长度不能超过64k
若是sendto给与的数据大于64k-8,则会发送失败
当发送的数据大于64k-8时,需要用户在应用层进行分包操作,分出的包大小不能大于64k-8
但是udp无法保证数据有序到达,因此当在应用层进行分包之后,还需要用户在应用层进行包序管理
传输层基于udp实现的应用层协议:DHCP,DNS

五.netstat工具
Netstat是控制台命令,是一个监控TCP/IP网络的非常有用的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。Netstat用于显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。

该命令的一般格式为 :netstat [-a][-e][-n][-o][-p Protocol][-r][-s][Interval]
命令中各选项的含义如下:
-a 显示所有socket,包括正在监听的。
-c 每隔1秒就重新显示一遍,直到用户中断它。
-i 显示所有网络接口的信息,格式“netstat -i”。
-n 以网络IP地址代替名称,显示出网络连接情形。
-r显示核心路由表,格式同“route -e”。
-t 显示TCP协议的连接情况
-u 显示UDP协议的连接情况。
-v 显示正在进行的工作。
-p 显示建立相关连接的程序名和PID。
-b 显示在创建每个连接或侦听端口时涉及的可执行程序。
-e 显示以太网统计。此选项可以与 -s 选项结合使用。
-f 显示外部地址的完全限定域名(FQDN)。
-o显示与与网络计时器相关的信息。-s 显示每个协议的统计。
-x 显示 NetworkDirect 连接、侦听器和共享端点。
-y 显示所有连接的 TCP 连接模板。无法与其他选项结合使用。
interval 重新显示选定的统计,各个显示间暂停的 间隔秒数。按 CTRL+C 停止重新显示统计。如果省略,则 netstat 将打印当前的配置信息一次。

常用选项n
etstat -a
本选项显示一个所有的有效连接信息列表,包括已建立的连接(ESTABLISHED),也包括监听连接请求(LISTENING)的那些连接。

netstat -b
该参数可显示在创建网络连接和侦听端口时所涉及的可执行程序。

netstat -s
本选项能够按照各个协议分别显示其统计数据。如果你的应用程序(如Web浏览器)运行速度比较慢,或者不能显示Web页之类的数据,那么你就可以用本选项来查看一下所显示的信息。你需要仔细查看统计数据的各行,找到出错的关键字,进而确定问题所在。

netstat -e
本选项用于显示关于以太网的统计数据,它列出的项目包括传送数据报的总字节数、错误数、删除数,包括发送和接收量(如发送和接收的字节数、数据包数 [1] ),或有广播的数量。可以用来统计一些基本的网络流量。

netstat -r
本选项可以显示关于路由表的信息,类似于后面所讲使用routeprint命令时看到的信息。除了显示有效路由外,还显示当前有效的连接。

netstat -n
显示所有已建立的有效连接。

netstat -p
显示协议名查看某协议使用情况

常见状态
即连接状态。在原模式中没有状态,在用户数据报协议中也经常没有状态,于是状态列可以空出来。若有状态,通常取值为:
LISTEN
侦听来自远方的TCP端口的连接请求

SYN-SENT
在发送连接请求后等待匹配的连接请求

SYN-RECEIVED
在收到和发送一个连接请求后等待对方对连接请求的确认

ESTABLISHED
代表一个打开的连接

FIN-WAIT-1
等待远程TCP连接中断请求,或先前的连接中断请求的确认

FIN-WAIT-2
从远程TCP等待连接中断请求

CLOSE-WAIT
等待从本地用户发来的连接中断请求

CLOSING
等待远程TCP对连接中断的确认

LAST-ACK
等待原来的发向远程TCP的连接中断请求的确认

TIME-WAIT
等待足够的时间以确保远程TCP接收到连接中断请求的确认

CLOSED
没有任何连接状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值