Linux套接字编程

文章目录

  • Socket网络编程
    • IP地址:
    • 端口
    • 网络字节序:
    • 传输层协议选择
    • UDP编程
      • API接口介绍
        • 1. 创建套接字
        • 2. 为套接字绑定地址信息
        • 3. 接受数据
        • 4. 发送数据
        • 5. 关闭套接字
        • API流程图
      • UDP编程流程图
      • UDP模拟实现
    • TCP编程
      • TCP编程流程
      • tcp服务端程序
      • tcp客户端程序
      • TCP模拟实现
      • TCP连接断开
      • 多进程tcp服务端程序/多线程tcp服务端程序
        • 多进程实现
        • 多线程实现

Socket网络编程

IP地址:

作用:在网络中唯一一台主机
类型:

  • IPV4版本:uint32_t - - - IP地址不够用 - - - DHCP-动态地址分配技术。
  • NAT:地址转换技术
  • IPV6版本:uint8_t addr[16] - - -不向前兼容IPV4(没有推广使用—成本问题)
发送sip,sport dip dport实际路径
发送端1
接收端B
发送端1
路由器
接收端2
互联网
qq
互联网
路由器
接收端2

Linux套接字编程_第1张图片
每一条数据都会包含:源IP地址与目的IP地址

端口

在一台主机上标识一个进程(发往哪个端口的数据应该由哪个进程来处理)

  • 端口号是一个2字节16位的整数:uint16_t;端口的范围:0 ~ 65535,其中0 ~ 1023这些端口不推荐用户使用,因为0 ~ 1023已经被一些知名协议所占用,或作为预留
  • 一个端口只能被一个进程占用
  • 一个进程可以使用多个端口,但是一个端口号不能被多个进程绑定;
  • 每一条数据都会包含:源端口与目的端口
  • 每条数据中:(源IP,源端口,目的IP,目的端口,协议)构成五元组:标识网络中的一条通信

理解源端口号和目的端口号唐僧例子、送快递例子
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”

网络字节序:

字节序:CPU在内存中对数据进行存取的顺序
主机字节序:0x 01 02 03 04

大端字节序:低地址存高位 uint_8 a[4] a[0]=01, a[1]=02, a[2]=03, a[3]=04

小端字节序:低地址存地位 uint_8 a[4] a[0]=04, a[1]=03, a[2]=02, a[3]=01

主机字节序决定于CPU架构:intel的CPU架构X86(小端),MIPS(大端)
在这里插入图片描述

不同主机字节序的主机传输数据(大于一个字节)会存在二义性(数据存取是按字节处理—因此字节序针对的是大于一个字节存储的类型)
short int long float double

网络数据传输中,有可能因为通信双方主机字节序不同而造成数据二义
因此规定在网络通信中使用大端字节序作为网络字节序成为通信的字节序标准

如何判断主机字节序? union联合体

union
{
    char a;如果是01就是小端,如果是00就是大端
    int b;b存00 00 00 01;
}u
u.b=00000001
if(u.a==01)
{//小端0}
if(u.a==00{//大端}

或类型强转:int a=1   uchar*b=(uchar*)&a

传输层协议选择

TCP协议:

  • 传输控制协议:面向连接,可靠传输,面向字节流
  • 实现数据可靠传输,传输灵活但是会有粘包问题

UDP协议:

  • 用户数据报协议:无连接,不可靠,面向数据报
  • 实现不可靠传输,传输不够灵活,但是不会存在粘包问题

TCP因为为了实现可靠传输,因此牺牲了部分性能,适用于安全性要求高的传输场景:文件传输

UDP因为你不用实现可靠传输,因此传输速度块,适用于实时性要求高的传输场景:视频传输


UDP编程

API接口介绍

1. 创建套接字

通过套接字使进程与网卡建立联系:内核中创建socket结构体

#include           /* See NOTES */
#include 
int socket(int domain, int type, int protocol);

domain: 地址域       AF_INET—使用IPV4网络协议地址域

type: 套接字类型

  • SOCK_STREAM:流式套接字,提供字节流服务,默认使用TCP协议
  • SOCK_DGRAM:数据包套接字,提供数据报传输服务,默认使用UDP协议

protocol: 传输层协议 协议声明in.h

		  0             根据套接字类型使用默认协议

TCP协议:
    IPPROTO_TCP = 6,	   /* Transmission Control Protocol.  */
#define IPPROTO_TCP		IPPROTO_TCP

UDP协议:
    IPPROTO_UDP = 17,	   /* User Datagram Protocol.  */
#define IPPROTO_UDP		IPPROTO_UDP

返回值:

RETURN VALUE:
       On  success,  a file descriptor for the new socket is returned.
       On error, -1 is returned, and errno is set appropriately.
  • 成功:套接字描述符(文件描述符)
  • 失败:-1

2. 为套接字绑定地址信息

#include           /* See NOTES */
#include 
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockfd: 创建套接字返回的套接字描述符
addr: 地址信息
addrlen: 地址信息长度
返回值:

  • 成功:返回0
  • 失败:返回-1

3. 接受数据

#include 
#include 
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

==使用recvfrom==
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

**sockfd:**套接字描述符
buf: 用于接收数据的缓冲区
len: 想要接收的数据长度(主要作用是为了限制buf防止越界)
flags: 选项标志

  • 0—>阻塞接收数据

src_addr: 发送端地址信息
addrlen: 地址信息长度(输入输出型参数:指定地址想要的长度,返回实际的长度)
返回值:

  • 成功:实际接收的数据长度
  • 失败:-1

4. 发送数据

#include 
#include 
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

==使用sendto==
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

sockfd: 套接字描述符
buf: 要发送的数据
len: 要发送的数据长度
flags:

  • 0—>默认阻塞发送数据

dest_addr: 目的端地址
addrlen: 地址长度
返回值:

  • 成功:实际的发送数据长度
  • 失败:-1

5. 关闭套接字

#include 
int close(int fd);

fd: 套接字描述符
返回值:

  • 成功:0
  • 失败:-1

API流程图

Linux套接字编程_第2张图片
按地址解析:
通过地址域判断是哪一个地址域,再按照指定的地址域去解析剩下的数据
Linux套接字编程_第3张图片
端口解析:
因为端口和地址都是要在网络上传输的,因此需要字节序转换

 #include 
uint32_t htonl(uint32_t hostlong);
//将32位的数据从主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort);
//将16位的数据从主机字节序转换为网络字
uint32_t ntohl(uint32_t netlong);
//将32位的数据从网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort);
//将16位的数据从网络字节序转换为主机字节序

Linux套接字编程_第4张图片

#include 
#include 
#include 

int inet_aton(const char *cp, struct in_addr *inp);

in_addr_t inet_addr(const char *cp);
//将字符串点分十进制IP地址转换为网络字节序IP地址

in_addr_t inet_network(const char *cp);

char *inet_ntoa(struct in_addr in);
//通过网络字节序IP地址转换为字符串点分十进制IP地址

struct in_addr inet_makeaddr(int net, int host);

n_addr_t inet_lnaof(struct in_addr in);

in_addr_t inet_netof(struct in_addr in);

Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

inet_aton(), inet_ntoa(): _BSD_SOURCE || _SVID_SOURCE

UDP编程流程图

Linux套接字编程_第5张图片

UDP模拟实现

UDP模拟实现


TCP编程

tcp: 面向链接,可靠传输,面向字节流

TCP编程流程

Linux套接字编程_第6张图片

tcp服务端程序

1. 创建套接字:

socket(AF_INT,SOCK_STREAM,IPPROTO_TCP)

2. 为套接字绑定地址信息

bind(sockfd,struct sockaddr_in *addr,addrlen)

3. 开始监听
listen(sockfd,backlog)

#include           /* See NOTES */
#include 
int listen(int sockfd, int backlog);

backlog:

  • 最大并发连接数-决定内核中已完成连接队列节点个数
  • 决定的不是服务端能接受的客户端最大上限,而是并发的最大上限

4. 获取完成连接的socket

#include           /* See NOTES */
#include 

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include 

int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);

sockfd: 套接字描述符
addr: 新建连接的客户端地址信息
addrlen: 新建客户端的地址信息长度
返回值: 返回新建客户端socket的描述符

int newsockfd=accept(sockfd,*client_addr,*addrlen)

新的sockfd仅仅用于与指定客户端进行单独通信

向服务端发送请求

#include           /* See NOTES */
#include 

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockfd: 套接字描述符
addr: 服务端地址信息
addrlen: 地址信息长度

5. 接受数据
recv(newsockfd,buf,len,flag)
6. 发送数据
send(newsockfd,data,len,flag)

while(1){
       获取已完成连接的新客户端:socket clientfd=accept(sockfd,cliaddr,addrlen)
       接收数据:revc(clientfd,buf,buflen,flag)
             返回值:
             	>0实际接收字节         
             	==0连接断开         
             	<0出错
       发送数据:send(clientfd,data,datalen,flag)
}

7. 关闭连接
close(newsockfd)
close(sockfd)

tcp客户端程序

1. 创建套接字
2. 绑定地址信息

不推荐手动绑定固定地址

3. 向服务端发起连接请求
connect(sockfd,srvaddr,addrlen)
4. 发送数据
5. 接受数据
6. 关闭socket

TCP模拟实现

TCP模拟实现

TCP连接断开

tcp自己实现了报货机制:当长时间没有数据通信,服务端会想客户端发送保活探测包;当这些保活探测包连续多次都没有响应,则认为连接断开

recv返回0;send触发异常

最基本的tcp服务端程序同一时间只能与一个客户端通信一次
因为用户不知道客户端什么时候有数据到来/不知道什么时候有新的客户端连接请求到来;所以程序只能按照固定套路流程运行;这时候程序的流程就有可能因为accept/recv函数的阻塞特性导致程序阻塞,导致服务端无法同时处理多个客户端请求
解决方法:
多线程或多进程

多进程tcp服务端程序/多线程tcp服务端程序

使用多进程tcp服务端程序的处理多客户端请求;每当一个客户端的连接到来,都创建一个新的子进程,让子进程单独与客户端进行通信;这样的话父进程永远只处理新连接
Linux套接字编程_第7张图片
当获取到已完成连接的客户端socket,则为这个新的socket创建一个进程/线程来单独为这个客户端服务(通信)
对主进程/父进程来说要做的事情就一件:处理连接,获取连接
多进程/多线程能够解决服务器同时处理多个客户端数据的本质:是每个线程/进程都是一个独立的执行流
Linux套接字编程_第8张图片

多进程实现

#include "tcpsocket.hpp"
#include 
#include 

void sigcb(int no)
{
  while(waitpid(-1,NULL,WNOHANG)>0);
}
int main(int argc,char* argv[])
{
  if(argc!=3)
  {
    std::cout<<"./tcp_server 192.168.32.130 9000\n";
    return -1;
  }
  std::string ip=argv[1];
  uint16_t port=atoi(argv[2]);

  signal(SIGCHLD,sigcb);
  TCPSocket sock;

  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind(ip,port));
  CHECK_RET(sock.Listen());
  while(1)
  {
    TCPSocket clientsock;
    std::string client_ip;
    uint16_t client_port;
    if(sock.Accept(clientsock,client_ip,client_port)==false)
    {
      continue;
    }
    std::cout<<"new client:"<<client_ip<<":"<<client_port<<std::endl;
    
    int pid=fork();
    if(pid==0)
    {   
      while(1)
      {
        std::string buf;
        clientsock.Recv(buf);
        std::cout<<"client say:"<<buf<<std::endl;
        buf.clear();
        std::cout<<"server say:";
        fflush(stdout);
        std::cin>>buf;
        clientsock.Send(buf);
      }
      clientsock.Close();
      exit(0);
    }
    //父进程一定要关闭这个套接字;因为父子进程数据独有
    //父进程关闭对子进程无影响;不关闭会造成资源泄漏
    clientsock.Close();
  }
  sock.Close();
  return 0;
}

多线程实现

#include "tcpsocket.hpp"
#include 

void *thr_start(void*arg)
{
  //因为线程之间,共享文件描述符表,因此在一个线程中打卡的文件
  //另一个线程只要能够获取到文件描述符,就能操作文件
  TCPSocket *clientsock=(TCPSocket*)arg;
  while(1)
  {
    std::string buf;
    clientsock->Recv(buf);
    std::cout<<"client say:"<<buf<<std::endl;
    buf.clear();
    std::cout<<"server say:";
    fflush(stdout);
    std::cin>>buf;
    clientsock->Send(buf);
  }
  clientsock->Close();
  return NULL;
}
int main(int argc,char* argv[])
{
  if(argc!=3)
  {
    std::cout<<"./tcp_server 192.168.32.130 9000\n";
    return -1;
  }
  std::string ip=argv[1];
  uint16_t port=atoi(argv[2]);

  TCPSocket sock;

  CHECK_RET(sock.Socket());
  CHECK_RET(sock.Bind(ip,port));
  CHECK_RET(sock.Listen());
  while(1)
  {
    TCPSocket *clientsock=new TCPSocket();
    std::string client_ip;
    uint16_t client_port;
    if(sock.Accept(*clientsock,client_ip,client_port)==false)
    {
      continue;
    }
    std::cout<<"new client:"<<client_ip<<":"<<client_port<<std::endl;
    pthread_t tid;
    pthread_create(&tid,NULL,thr_start,(void*)clientsock);
    pthread_detach(tid);
  }
  sock.Close();
  return 0;
}

你可能感兴趣的:(Linux网络)