C++网络编程——socket

在服务器中,需要建立一个socket套接字才能对外提供一个网络通信接口,在Linux系统中套接字仅是一个文件描述符,也就是一个int类型的值

socket概念

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。

我们把插头插到插座上就能从电网上获得电力供应,同样为了与远程计算机进行数据传输,需要连接到因特网,而socket就是用来连接到因特网的工具

UNIX/Linux下的socket

在UNIX/Linux下,一切都是文件

为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:

  • 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;

  • 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。

UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。

请注意,网络连接也是一个文件,它也有文件描述符

Linux下socket使用

创建

#include<sys/socket.h>
int sockfd = socket(AF_INET,SOCK_STREAM,0);
  • 创建socket的API中第一个参数是IP地址类型,AF_INET表示使用IPv4,如果使用IPv6请使用AF_INET6,另外AF_UNIX则是Unix域套接字,即本地套接字

  • 第二个是参数为数据传输方式,SOCK_STREAM表示流格式、面向连接,多用于TCP。SOCK_DGRAM表示数据报格式、无连接,多用于UDP,至于TCP为什么为流格式,UDP为什么为面向报文,在专栏计算机网络部分为有详细解释。

  • 第三个参数:协议,0表示根据前面的两个参数自动推导协议类型。设置为IPPROTO_TCP和IPPTOTO_UDP,分别表示TCP和UDP。

sockadd_in结构体

对于客户端,服务器存在的唯一标识是一个IP地址和端口,这时候我们需要将这个套接字绑定到一个IP地址和端口上。首先创建一个sockaddr_in结构体

#include <arpa/inet.h>  //这个头文件包含了<netinet/in.h>,不用再次包含了
#include<string.h> //包含了bzero,如果是c使用string.h如果是C++使用cstring.h
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));

创建完后用bzero初始化这个结构体

sockaddr_in结构体内容如下:

struct sockaddr_in {
    sa_family_t sin_family;  // 地址族,通常设置为 AF_INET
    in_port_t sin_port;      // 端口号,网络字节序
    struct in_addr sin_addr; // IPv4 地址
};

其中IPV4地址结构体如下:

struct in_addr {
    in_addr_t s_addr; // 32位的 IPv4 地址,采用网络字节序
};

另外如果是在IPV6或者UNIX下本地套接字的话,要使用另外的结构体

  • IPV6:

struct sockaddr_in6 {
    sa_family_t     sin6_family;  // 地址族,通常设置为 AF_INET6
    in_port_t       sin6_port;    // 端口号,网络字节序
    uint32_t        sin6_flowinfo;// 流信息
    struct in6_addr sin6_addr;    // IPv6 地址
    uint32_t        sin6_scope_id;// 作用域ID
};
​
struct in6_addr {
    unsigned char   s6_addr[16]; // 128位的 IPv6 地址
};
  • UNIX:

struct sockaddr_un {
    sa_family_t sun_family; // 地址族,通常设置为 AF_UNIX
    char        sun_path[108]; // Unix socket 的路径名
};

通用的sockaddr结构体

struct sockaddr {
    sa_family_t sa_family;  // 地址族
    char        sa_data[14]; // 地址信息
};
  1. sa_family

    • 表示地址族,通常取值为 AF_INETAF_INET6AF_UNIX 等。

    • 用于确定后续的地址信息如何解释。

  2. sa_data

    • 一个14字节的地址信息数组。

    • 具体的地址信息格式取决于地址族。

在初始化IPV4的结构体后,就要对其设置地址族,IP地址和端口

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);

绑定

然后将socket地址与文件描述符绑定:

bind(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));

为什么定义的时候使用专用socket地址,而绑定的时候转换为通用socket地址(sockaddr),这些内容在游双《Linux高性能服务器编程》第五章第一节:socket地址API中有详细讨论,我这里也进行一部分的引用加以说明

“通用socket地址结构体显然很不好用,比如设置与获取 IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提 供了专门的socket地址结构体,同时所有专用socket地址类型的变量在实际使 用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因 为所有socket编程接口使用的地址参数的类型都是sockaddr。”

inet_addr是将char类型字符串转变为网络字节序

#include <sys/socket.h>
​
#include <netinet/in.h>
​
#include <arpa/inet.h>
​
in_addr_t inet_addr(const char *cp); //将char类型字符串转变为网络字节序
​
char *inet_ntoa(struct in_addr in);  //将网络字节序转换为char类型

不过现在用这些比较少,

#include <arpa/inet.h>
    int inet_pton(int af, const char *src, void *dst);
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
      //af参数指明IP地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
      //src参数是指向二进制IP地址的指针。
      //dst是存储转换后的文本IP地址的缓冲区。
      //size参数指明dst缓冲区的大小。
      //该函数返回一个指向dst的指针,如果失败则返回NULL并设置errno。
    int inet_pton(int af, const char *src, void *dst);
      //af参数指明IP地址族,可以是AF_INET(IPv4)或AF_INET6(IPv6)。
      //src参数是指向文本IP地址的指针。
      //dst是存储转换后的二进制IP地址的缓冲区。
      //该函数返回值:
      //如果转换成功,返回1。
      //如果src参数不是有效的IP地址字符串,返回0。
      //如果出错,返回-1并设置errno。

例如:

#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<arpa/inet.h>
​
int main(){
  char buf[] = "192.168.1.2"; //低字节为2,高字节为192
  unsigned int name = 0;
  inet_pton(AF_INET,buf,&name);  //将点分十进制转换为网络字节序,大端存储,发送时用的多
  printf("name = %d\n",name); //0x0201a8c0
  unsigned char* p = (unsigned char*) &name;
  printf("%d,%d,%d,%d\n",*p,*(p+1),*(p+2),*(p+3)); //大端存储
  char ip[16] = "";
  inet_ntop(AF_INET,&name,ip,16); //将网络字节序转换为点分十进制  ,接收时用的多
  printf("%s",ip);
}

打印结果为:

name = 33663168 
192,168,1,2
192.168.1.2

监听

使用listen函数监听这个socket端口,这个函数的第二个参数是listen函数的最大监听队列长度,系统建议的最大值SOMAXCONN被定义为128。

listen(sockfd, SOMAXCONN);

要接受一个客户端连接,需要使用accept函数。对于每一个客户端,我们在接受连接时也需要保存客户端的socket地址信息,于是有以下代码:

struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len = sizeof(clnt_addr);
bzero(&clnt_addr, sizeof(clnt_addr));
int clnt_sockfd = accept(sockfd, (sockaddr*)&clnt_addr, &clnt_addr_len);
printf("new client fd %d! IP: %s Port: %d\n", clnt_sockfd, inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));

要注意和acceptbind的第三个参数有一点区别,对于bind只需要传入serv_addr的大小即可,而accept需要写入客户端socket长度,所以需要定义一个类型为socklen_t的变量,并传入这个变量的地址。另外,accept函数会阻塞当前程序,直到有一个客户端socket被接受后程序才会往下运行。

现在,客户端已经可以通过IP地址和端口号连接到这个socket端口了,让我们写一个测试客户端连接试试:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(8888);
connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr));  

代码和服务器代码几乎一样:创建一个socket文件描述符,与一个IP地址和端口绑定,最后并不是监听这个端口,而是使用connect函数尝试连接这个服务器。

运行编译出来的./server和./client可以看到服务器接收到了客户端的连接请求,并成功连接

new client fd 3! IP: 127.0.0.1 Port: 53505

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值