c++ socket 学习

c++ Socket学习

int socket(int af, int type, int protocol);

af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。

type 为数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM

protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

bind()函数

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));  //每个字节都用0填充
serv_addr.sin_family = AF_INET;  //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //具体的IP地址
serv_addr.sin_port = htons(1234);  //端口

//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

sockaddr_in 结构体

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};

sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。

sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。

端口号需要用 htons() 函数转换,后面会讲解为什么。

sin_addr 是 struct in_addr 结构体类型的变量,下面会详细讲解。

sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

in_addr 结构体

sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员,如下所示:

struct in_addr{    
  in_addr_t  s_addr;  //32位的IP地址
};

in_addr_t 在头文件 中定义,等价于 unsigned long,长度为4个字节。也就是说,s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换,例如:

为什么使用 sockaddr_in 而不使用 sockaddr

bind() 第二个参数的类型为 sockaddr,而代码中却使用 sockaddr_in,然后再强制转换为 sockaddr,这是为什么呢?

sockaddr 结构体的定义如下:

struct sockaddr{    
  sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型    char         
  sa_data[14];  //IP地址和端口号
};

下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):


img

sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。

可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下:

struct sockaddr_in6 {     
  sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6    
  in_port_t sin6_port;  //(2)16位端口号    
  uint32_t sin6_flowinfo;  //(4)IPv6流信息    
  struct in6_addr sin6_addr;  //(4)具体的IPv6地址    
  uint32_t sin6_scope_id;  //(4)接口范围ID
};

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

connect() 函数

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);  //Linux

listen() 函数

通过 listen() 函数可以让套接字进入被动监听状态,它的原型为:

int listen(int sock, int backlog);  //Linux

accept() 函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,

Linux下数据的接收和发送

Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据

write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

使用 shutdown() 函数

int shutdown(int sock, int howto);  //Linux

howto 在 Linux 下有以下取值:

  • SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
  • SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
  • SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close()

默认情况下,close()会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

socket文件传输功能的实现

server.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
const int bufferSize = 1024;
int main(){
        char *filename = (char *)"send.txt";
    FILE *fp = fopen(filename, "rb");//二进制
    if(fp == NULL){
        std::cout<<"No This File"<

client.cpp 略

在socket中使用域名

客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。

而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用。

关于域名注册、域名解析、host 文件、DNS 服务器等本节并未详细讲解,请读者自行脑补。本节重点讲解如何使用域名。

通过域名获取IP地址

域名仅仅是IP地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成IP地址。

gethostbyname() 函数可以完成这种转换,它的原型为:

struct hostent *gethostbyname(const char *hostname);

hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的IP地址。返回的地址信息会装入 hostent 结构体,该结构体的定义如下:

struct hostent{    
  char *h_name;  //official name    
  char **h_aliases;  //alias list    
  int  h_addrtype;  //host address type    
  int  h_length;  //address lenght    
  char **h_addr_list;  //address list
}

从该结构体可以看出,不只返回IP地址,还会附带其他信息,各位读者只需关注最后一个成员 h_addr_list。下面是对各成员的说明:

  • h_name:官方域名(Official domain name)。官方域名代表某一主页,但实际上一些著名公司的域名并未用官方域名注册。
  • h_aliases:别名,可以通过多个域名访问同一主机。同一IP地址可以绑定多个域名,因此除了当前域名还可以指定其他域名。
  • h_addrtype:gethostbyname() 不仅支持 IPv4,还支持 IPv6,可以通过此成员获取IP地址的地址族(地址类型)信息,IPv4 对应 AF_INET,IPv6 对应 AF_INET6。
  • h_length:保存IP地址长度。IPv4 的长度为4个字节,IPv6 的长度为16个字节。
  • h_addr_list:这是最重要的成员。通过该成员以整数形式保存域名对应的IP地址。对于用户较多的服务器,可能会分配多个IP地址给同一域名,利用多个服务器进行均衡负载。

UDP Socket

UDP 是非连接的传输协议,没有建立连接和断开连接的过程,它只是简单地把数据丢到网络中,也不需要ACK包确认。

server.cpp

#include 
#include 
#include 
#include 
#include 
#include 
const int bufferSize = 1024;
int main(){
    int m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    struct sockaddr_in m_addr;
    memset(&m_addr,0,sizeof(m_addr));
    m_addr.sin_family = AF_INET;
    m_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    m_addr.sin_port = htons(8000);
    bind(m_socket, (struct sockaddr*)&m_addr, sizeof(sockaddr));
    
    sockaddr client_addr;
    socklen_t client_addr_size = sizeof(client_addr);
    char buffer[bufferSize] = {0};
    while(1){
        int strlen = recvfrom(m_socket, buffer, bufferSize, 0, &client_addr, &client_addr_size);
        sendto(m_socket, buffer, strlen, 0, &client_addr, client_addr_size);
    }
    close(m_socket);
}

client.cpp

#include 
#include 
#include 
#include 
#include 
#include 
const int bufferSize = 1024;
int main(){
    int m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("192.168.0.105");
    server_addr.sin_port = htons(8000);
    struct sockaddr_in from_addr;
    socklen_t from_addr_size = sizeof(from_addr);
    while (1) {
        char buffer[bufferSize] = {0};
        printf("Input message:");
        gets(buffer);
        sendto(m_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
        int strLen = recvfrom(m_socket, buffer, bufferSize, 0, (struct sockaddr*)&from_addr, &from_addr_size);
        //buffer[strLen] = 0;
        printf("message from: %s is %s\n",inet_ntoa(from_addr.sin_addr) ,buffer);
    }
}

你可能感兴趣的:(c++ socket 学习)