linux系统编程 socket part1


参考:李慧琴老师视频 + 笔记: Linux系统编程学习笔记
仅作自己学习笔记使用,侵删

引言

上一章考察了各种UNIX系统所提供的经典进程间通信机制(IPC):管道、FIFO、消息队列、信号量以及共享存储。这些机制允许在同一台计算机上运行的进程可以相互通信。本章将考察**不同计算机(通过网络相连)**上的进程相互通信的机制:网络进程间通信(network IPC)。

在本章中,我们将描述套接字网络进程间通信接口,进程用该接口能够和其他进程通信,无论它们是在同一台计算机上还是在不同的计算机上。实际上,这正是套接字接口的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信。尽管套接口可以采用许多不同的网络协议进行通信,但本章的讨论限制在因特网事实上的通信标准:TCP/IP协议栈。

协议的制定(讨论:跨主机传输需要注意的问题)

1.字节序问题

大端:低地址处存放高字节
小端:低地址处存放低字节(x86)
不管是文件传输,还是io实现,永远是低地址处的数据先出去
所以接收端如果接收到大端数据,按小端去解析,数值差距很大

假设要存放的数据是 0x30313233,那么 33 是低位,30 是高位,(左高右低)。在大端存储格式中,30 存放在低位,33 存放在高位;而在小端存储格式中,33 存放在低位,30 存放在高位。


主机字节序:host
网络字节序:network
_ to _ _(长度,要么s 两个字节16位,要么l 四个字节32位): ntohl, ntohs, htons, htonl

2.对齐

在32位的机器上,各占用4,4,1共9个字节的大小。但是编译器会将其自动对齐,此时为12个字节。

struct 
{
	int i;
	float f;
	char ch;
}

对齐理解:如果当前的起始地址号能够整除这个成员的sizeof

对齐原因

  • 平台原因: 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

总体来说:结构体的内存对齐是拿空间来换取时间的做法。优点是提高了可移植性和cpu性能。

解决办法:不对齐
网络传输的结构体中的成员都是紧凑的,所以不能地址对齐,需要在结构体外面增加 attribute((packed))。例如:

struct msg_st {
     uint8_t name[NAMESIZE];
     uint32_t math;
     uint32_t chinese;
}__attribute__((packed));

3.类型长度问题

标准C并没有对int、char这样的基本数据类型占用多大字节做一个明确的规定,例如:

一个16位的机器上,int可能占2个字节;
一个32位的机器上,int可能占4个字节;

解决:使用int32_tuint32_tint64_tint8_tuint8_t等类型明确指定占用的位数。这些类型包含在头文件<stdint.h>中。

SOKECT是什么

套接字是一种通信机制(通信的两方的一种约定),socket屏蔽了各个协议的通信细节,提供了tcp/ip协议的抽象,对外提供了一套接口,通过这个接口就可以统一、方便的使用tcp/ip协议的功能。这使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。我们可以用套接字中的相关函数来完成通信过程。

在这里插入图片描述

1.套接字描述符

套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read和write)可以用于处理套接字描述符。

为创建一个套接字,调用socket函数:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

我要用domain协议族中的protocol协议,实现type类型的传输

  • domain:域,或者协议族,确定通信的特性。即采用什么协议来传输数据。
    • AF_UNIX、AF_LOCAL:本地协议;
    • AF_INET:IPV4 协议;
    • AF_INET6:IPV6 协议;
    • AF_IPX:是非常古老的操作系统,出现在 TCP/IP 之前
    • AF_NETLINK:是用户态与内核态通信的协议;
    • AF_APPLETALK:苹果使用的一个局域网协议;
    • AF_PACKET:底层 socket所用到的协议,比如抓包器所遵循的协议一定要在网卡驱动层,而不能在应用层,否则无法见到包封装的过程。再比如 ping 命令,想要实现ping 命令就需要了解这个协议族。
  • type:确定套接字的类型,进一步确定通信特征。
    • SOCK_STREAM:流式套接字,特点是有序、可靠。有序、双工、基于连接的、以字节流为单位的。
      • 可靠不是指不丢包,而是流式套接字保证只要你能接收到这个包,那么包中的数据的完整性一定是正确的。
      • 双工是指双方都能收发。
      • 基于连接的是指:通信双方是知道对方是谁。
      • 字节流是指数据没有明显的界限,一端数据可以分为任意多个包发送。
    • SOCK_DGRAM:报式套接字(数据报套接字),比如结构体,无连接的,固定的最大长度,不可靠的消息。
    • SOCK_SEQPACKET:提供有序、可靠、双向、基于连接的数据报通信。(类似消息队列)
    • SOCK_RAW:原始的套接字,提供的是网络协议层的访问。
    • SOCK_RDM:数据层的访问,不保证传输顺序。
  • protocol:具体使用哪个协议。在 domain 的协议族中每一个对应的 type 都有一个或多个协议,使用协议族中默认的协议可以填写0
  • 返回值:如果成功,返回套接字描述符;如果失败,返回 -1,并设置 errno。

调用socket与调用open相类似。在两种情况下,均可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。

2.主动端和被动端(以下均以报式套接字为例)

主动端
1.取得socket
2.给socket关联绑定地址(可以省略)
3.发/收消息
4.关闭socket

被动端
1.取得socket
2.给socket关联绑定地址
3.收/发消息
4.关闭socket

为什么主动端绑定地址可以省略?
主动端可省略,不必与操作系统约定端口,由操作系统指定随机端口。

3.寻址

标识目标通信进程需要网络地址(IP)和端口号(port),前者标识网络上想与之通信的计算机,后者帮助标识特定的进程,因此需要将套接字与这两者进行绑定关联。

补充知识点:void*

C++中的void*理解

  1. void*类型的指针其实本质就是一个过渡型的指针状态,必须要**赋予类型(强制类型转换)**才能正常使用。
    void *的范围较大,所以强制转换,使其进行范围缩小。
    在这里插入图片描述
  2. 只能单向类型转换void可以转化成其他类型,但是有类型的不能转化成void
    任何类型的指针都可以直接赋值给void* 型指针,无需进行强制类型转换,相当于void *包含了其他类型的指针。
  3. 在函数调用过程中的使用作为输入输出参数,表示可以接受任意类型的输入指针和输出任意类型的指针,可以灵活使用任意类型的指针,避免只能使用固定类型的指针。
3.1 地址格式

在用bind函数绑定地址时,不同的协议有不同的地址类型

NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

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

The actual structure passed for the addr argument will depend on the address family. The sockaddr structure is defined as something like:

 struct sockaddr {
       sa_family_t sa_family;
       char        sa_data[14];
   }

The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings.

不同的协议族有不同的sockaddr, 以 AF_INET 为例:

   struct sockaddr_in {
       sa_family_t    sin_family; /* address family: AF_INET */
       in_port_t      sin_port;   /* port in network byte order */
       struct in_addr sin_addr;   /* internet address */
   };
   /* Internet address. */
   struct in_addr {
       uint32_t       s_addr;     /* address in network byte order */
   };

在填写sin_addr时,我们需要将点式地址转为uint_32类型地址,那么可以借助函数inet_pton
将点分十进制的文本字符串格式转换为网络字节序的地址(一个uint32_t的大整数),放在addr中

NAME
       inet_pton - convert IPv4 and IPv6 addresses from text to binary form

SYNOPSIS
       #include <arpa/inet.h>

       int inet_pton(int af, const char *src, void *dst);
  • af:协议族,仅支持以下两种:
    • AF_INET:IPV4 协议
    • AF_INET6:IPV6 协议
  • src: 点分十进制的文本字符串
  • dst: 存放转换后网络字节序二进制的地址

那么主动端的寻址如下

#define PORT 1986;
sockadd_in laddr;
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(PORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(af, "0.0.0.0", &laddr.sin.addr.s_addr); 

0.0.0.0还可以如下定义:

/* Address to accept any incoming messages.  */
#define INADDR_ANY    ((unit_32) 0x00000000)

绑定如下:

bind(sd, void(*)&laddr, sizeof(laddr));

4.接收和发送

函数参数的区别:
1.报式套接字
需要对每一条消息都记录是从哪来的,需要对端的地址和地址长度
2.流式套接字
已经建立好一对一的连接了,所以不需要对端地址和长度

4.1 接收函数
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

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

       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, 有没有特殊要求 ,对端的地址,和地址长度

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
4.2 发送函数
NAME
       send, sendto, sendmsg - send a message on a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>

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

       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, 有没有特殊要求, 对端的地址, 和地址长度。简单起见,我们可以将对端的地址通过传参传递

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

5.报式套接字示例

proto.h

#ifndef __PROTO_H_
#define __PROTO_H_
#define RECVPORT 1986
#define NAMESIZE 11

struct msg_t
{
    //定长,不可能有负值
    uint8_t name[NAMESIZE];
    uint32_t chinese;
    uint32_t math;
}__attribute__((packed));

# endif

rcver.cpp

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

#include "proto.h"
#define IPSTRSIZE        64

int main() {
    // 套接字描述符
    int sd;
    
    // laddr 本机地址
    // raddr 对端地址
    sockaddr_in laddr, raddr;
    
    // 存储接收到的结构体
    msg_t mssg;

    // 存储对端地址,点分式
    char ipstr[IPSTRSIZE];
    
    // 创建socket,创建协议为ipv4的报式套接字,0为默认协议,即UDP
    sd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sd < 0) {
        perror("socker()");
        exit(1);
    }

    // 填写本机的地址信息
    laddr.sin_family = AF_INET;
    // ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
    laddr.sin_port = htons(RECVPORT);
    // 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
    // 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
    inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
    
    //绑定接收的ip地址和端口号
    if (bind(sd, (const sockaddr*)&laddr, sizeof(laddr)) < 0) {
        perror("sendto()");
        exit(1);
    }
    
    // 接收
    // !!!!这里一定要初始化对端地址的大小!!!
    socklen_t addr_len = sizeof(raddr);
    while (1) {
        if (recvfrom(sd, (void*)&mssg, sizeof(mssg), 0, (sockaddr*)&raddr, &addr_len) < 0) {
            perror("recvfrom()");
            exit(1);
        }
        inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
        std::cout << "---------recive message from " << std::string(ipstr) << ":" << ntohs(raddr.sin_port) << "---------" << std::endl;
        // 单字节传输不涉及到大端小端的存储情况
        std::cout << "name" << ":" << mssg.name << std::endl;
        std::cout << "math" << ":" << ntohs(mssg.math) << std::endl;
        std::cout << "chinese" << ":" << ntohs(mssg.chinese) << std::endl;
    }
    
    //关闭
    close(sd);
    exit(1);
}

运行编译好的代码,用新一个终端,使用命令netstat -anu查看,可以看到Local Address 0.0.0.0:1986

vratdrh7771.rsv.ven.veritas.com [66]: netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
udp        0      0 127.0.0.1:55212         127.0.0.1:55212         ESTABLISHED
udp        0      0 0.0.0.0:68              0.0.0.0:*
udp        0      0 0.0.0.0:111             0.0.0.0:*
udp        0      0 127.0.0.1:323           0.0.0.0:*
udp        0      0 0.0.0.0:752             0.0.0.0:*
udp        0      0 127.0.0.1:843           0.0.0.0:*
udp        0      0 0.0.0.0:42743           0.0.0.0:*
udp        0      0 0.0.0.0:1986            0.0.0.0:*

snder.cpp

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

#include "proto.h"

int main(int argc, char **argv) {
    int sd;
    msg_t sendmssg;
    struct sockaddr_in raddr;
    
    //创建socket
    sd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sd < 0) {
        perror("socker()");
        exit(1);
    }
    //本地绑定(可以省略)
    
    //填写发送消息
    memset(&sendmssg, '\0', sizeof(sendmssg));
    //strcpy(sendmssg.name, "tracy");
    memcpy(sendmssg.name, "tracy", sizeof("tracy"));
    sendmssg.chinese = ntohs(100);
    sendmssg.math = ntohs(100);
    
    //对端地址
    raddr.sin_family = AF_INET;
    raddr.sin_port = ntohs(RECVPORT);
    inet_pton(AF_INET, argv[1], &raddr.sin_addr);

    //发送
    if (sendto(sd, &sendmssg, sizeof(sendmssg), 0, (const sockaddr*)&raddr, sizeof(raddr)) < 0) {
        perror("sendto()");
        exit(1);
    }
    //关闭
    close(sd);
}

运行结果

vratdrh7771.rsv.ven.veritas.com [144]: ./rcver
---------recive message from 10.85.171.130:54485---------
name:tracy
math:100
chinese:100
---------recive message from 10.85.171.130:41376---------
name:tracy
math:100
chinese:100

vratdrh7771.rsv.ven.veritas.com [57]: ./snder 10.85.171.130
  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值