一、TCP网络编程基础

1.1、套接字编程基础知识

1.1.1、套接字地址结构

  1. 通用套接字数据结构 ```cpp struct sockaddr { _SOCKADDR_COMMON (sa); char sa_data[14]; };

define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

typedef unsigned short int sa_family_t;

  1. sockaddr的第一个字段实际上就是sa_family_t这个数据结构,而它也是unsigned short int的别名。<br />sockaddr的取值有:
  2. 2. 实际使用的套接字数据结构
  3. ```cpp
  4. int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
  5. #define __CONST_SOCKADDR_ARG const struct sockaddr *

但是__CONST_SOCKADDR_ARG的结构设置比较复杂,一般使用sockaddr_in这个结构体进行设置

struct sockaddr_in {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[
        sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE
        - sizeof (in_port_t) - sizeof (struct in_addr)
    ];
};

#define    __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
typedef unsigned short int sa_family_t;

typedef uint16_t in_port_t;

typedef uint32_t in_addr_t;
struct in_addr {
    in_addr_t s_addr;
};

#define __SOCKADDR_COMMON_SIZE    (sizeof (unsigned short int))

sockaddr_in的第一个字段sin_family为协议簇;第二个字段sin_port为端口号;第三个字段sin_addr为IP地址;第四个是保留字段。

  1. sockaddr和sockaddr_in的关系

image.png
可以发现这两个数据结构虽然字段数量不同,但是它们的大小是一样的,所以可以互相转换,数据不会丢失。

1.2、TCP网络编程流程

1.2.1、TCP网络编程架构

TCP一般使用CS结构进行开发。

  1. 服务端的程序设计模式

image.png
第一步调用socket函数,初始化套接字。
第二步调用bind函数,将套接字与端口绑定。
第三步调用listen函数,设置服务器的监听。
第四步调用accept函数,接收客户端的连接。
第五步调用read和write函数,与客户端进行通信。
第六步调用close函数,释放资源。

  1. 客户端的程序设计模式

image.png
第一步调用socket函数,初始化套接字。
第二步调用connect函数,连接服务端。
第三步调用write和read函数,与服务端进行通信。
第四步调用close函数,释放资源。

1.2.2、初始化套接字socket()

#include <sys/socket.h>
#include <sys/types.h>
int socket (int __domain, int __type, int __protocol);

第一个参数是协议簇;第二个参数是协议类型;第三个参数是套接字文件描述符。

第一个参数的取值
名称 含义
PF_UNIX,PF_LOCAL 本地通信
PF_INET IPv4协议
PF_INET6 IPv6协议
PF_IPX IPX协议
PF_NETLINK 内核用户界面设备
PF_X25 ITU-T X.25 / ISO-8208协议
PF_AX25 Amateur radio AX.25协议
PF_ATMPVC 原始ATM PVC访问
PF_APPLETALK Apple talk
PF_PACKET 底层包访问
第二个参数的取值
SOCK_STREAM TCP连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输。
SOCK_DGRAM 支持UDP连接。
SOCK_SEQPACKET 序列化包,提供一个序列化的、可靠的、双向的基于连接的数据传输通道,数据长度定长。每次调用读系统调用时需要将全部数据读出。
SOCK_RAW RAW类型,提供原始的网络协议访问。
SOCK_RDM 提供可靠的数据报文,不过数据可能乱序。
SOCK_PACKET 这是专用类型,不能在通用程序中使用。

并不是所有的协议簇都实现了这些类型。
第三个参数用于指定某个协议的特定类型,即type类型中的某个类型。但是某个协议通常只有一个类型,所以第三个参数一般设置为0。
通常socket会执行成功,此时的返回值意味着socket的文件描述符,但也有不成功的时候,这时需要通过返回值判断错误。

socket返回值及其含义
EACCES 没有权限建立指定的domain的type的socket
EAFNOSUPPORT 不支持所给的地址类型
EINVAL 不支持此协议或协议不可用
EMFILE 进程文件表溢出
ENFILE 已经到达系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM 内存不足
EPROTONOSUPPORT 指定的协议type在domain中不存在

1.2.3、绑定地址端口bind()

int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
#define __CONST_SOCKADDR_ARG    const struct sockaddr *

第一个参数是socket的描述符,即socket函数的返回值;第二个参数是sockaddr,即地址与端口的信息;第三个参数是sockaddr的长度。
当bind返回值为0的时候,表示绑定成功,-1表示失败。

bind返回值及其含义
EADDRINUSE 给定地址已经使用
EBADF sockfd不合法
EINVAL sockfd已经绑定到其他地址
ENPTSOCK sockfd是一个文件描述符,不是socket描述符
EACCES 地址被保护,权限不足
EADDRNOTAVAIL 接口不存在或者绑定地址不是本地
EFAULT my_addr指针超出用户空间
EINVAL 地址长度错误,或者socket不是AF_UNIX簇
ELOOP 解析my_addr是符号链接过多
ENAMETOOLONG my_addr过长
ENOENT 文件不存在
ENOMEM 内存不足
ENOTDIR 不是目录
EROFS socket节点应该在只读文件系统上

1.2.4、绑定本地端口listen()

int listen (int __fd, int __n);

第一个参数是服务端socket的描述符,第二个参数是等待队列最大长度。listen成功执行返回0,否则返回-1。且可以获得err值。

listen()函数的errno值及含义
含义
EBADF 另一个socket已经在同一端口监听
EBADF 参数sockfd不是合法的描述符
ENOTSOCK 参数sockfd不是代表socket的文件描述符
EOPNOTSUPP socket不支持listen操作

在接受连接之前,需要用listen来监听端口。listen函数进队SOCK_STREAM或SOCK_SEQPACKET协议有效。

1.2.5、接受一个网络请求accept()函数

int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
# define __SOCKADDR_ARG        struct sockaddr *__restrict

第一个参数是服务端socket描述符,第二个参数是out参数,存储客户端地址、端口的指针,第三个参数也是out参数,存储前一个参数的大小。accept成功执行返回客户端socket描述符,否则返回-1。且可以获得err值。

1.2.6、连接目标网络服务器connect()函数

int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
#define __CONST_SOCKADDR_ARG    const struct sockaddr *

第一个参数是客户端socket描述符,第二个参数是存储着服务端的ip和地址的sockaddr的指针,第三个参数是前一个参数的大小。connect函数执行成功返回0,否则返回-1。也可以获得err值。

connect()函数的errno值及含义
EACCES 在AF_UNIX协议簇中,使用路径名作为标识,EACCES表示目录不可写或不可访问
EACCES/EPERM 用户没有设置广播标志二连接广播地址或连接请求被防火墙限制
EADDRINUSE 本机地址已经在使用
EAFNOSUPPORT 参数serv_addr的域sa_family不正确
EAGAIN 本地端口不足
EALREADY socket是非阻塞类型并且前面的连接没有返回
EBADF 文件描述符不是合法的值
ECONNERFUSED 连接的主机地址没有侦听
EFAULT socket结构地址超出用户空间
EINPROGRESS socket是非阻塞模式,而链接不能立刻返回
EINTR 函数被信号中断
EISCONN socket已经连接
ENETUNREACH 网络不可达
ENOTSOCK 文件描述符不是一个socket
ETIMEDOUT 连接超时

1.2.7、写入函数write()

ssize_t write (int __fd, const void *__buf, size_t __n);

第一个参数是客户端socket描述符,第二个参数是要写入的数据的缓冲区,第三个参数是缓冲区的长度。返回成功写入的数据长度。

1.2.8、读取函数read()

ssize_t read (int __fd, void *__buf, size_t __nbytes);

第一个参数是客户端socket描述符,第二个参数是要读取的缓冲区,第三个参数是缓冲区的长度。返回成功读取的数据长度。

1.2.9、关闭套接字函数

int shutdown (int __fd, int __how);

第一个参数是要关闭的socket描述符,第二个参数是如何关闭。

  • SHUT_RD:值为0,表示切断读,之后不能使用该socket执行读操作。
  • SHUT_WR:值为1,表示切断写,之后不能使用该socket执行写操作。
  • SHUT_RDWR:值为2,表示切断读写,之后不能使用该socket执行读写操作。

shutdown执行成功返回0,否则返回-1。可以获得errno。

shutdown的errno值及其含义
EBADF 文件描述符不是合法的值
ENOTCONN socket没有连接
ENOTSOCK 第一个参数是一个文件,不是socket

1.3、服务端/客户端的简单例子

1.3.1、服务端的简单例子

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

int main(int argc, char const* argv[]) {
    int sock = socket(AF_INET, SOCK_STREAM, 0); // 获取socket,设置ipv4协议,tcp协议
    sockaddr_in server_addr_opt; //声明服务端配置
    server_addr_opt.sin_family = AF_INET; // 设置协议簇
    server_addr_opt.sin_port = htons(8080); // 设置端口
    server_addr_opt.sin_addr.s_addr = inet_addr("0.0.0.0"); // 设置IP地址
    int bind_status = bind(sock, (sockaddr*)(&server_addr_opt), sizeof(struct sockaddr)); //给socket绑定相应信息
    listen(sock, 20); // 开启监听,监听队列最大20

    sockaddr_in client_addr_opt; // 声明客户端配置
    socklen_t sock_size; 声明客户端配置大小
    int client_sock = accept(sock, (sockaddr*)(&client_addr_opt), &sock_size); // 接受客户端消息,并设置客户端配置,客户端配置大小
    char readBuf[2048]; // 设置读缓冲区
    read(client_sock, readBuf, sizeof(buf)); // 读取数据
    std::cout << buf << std::endl;
    char writeBuf[] = "HelloWorld"; // 设置写缓冲区
    write(client_sock, writeBuf, sizeof(writeBuf)); // 写数据
    shutdown(sock, SHUT_RDWR); // 关闭socket
    return 0;
}

1.3.2、客户端的简单例子

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

int main(int argc, char const* argv[]) {
    int client = socket(AF_INET, SOCK_STREAM, 0); // 获取客户端socket
    sockaddr_in clientOpt; // 声明客户端连接配置
    clientOpt.sin_addr.s_addr = inet_addr("127.0.0.1");
    clientOpt.sin_family = AF_INET;
    clientOpt.sin_port = htons(8080);
    connect(client, (sockaddr*)(&clientOpt), sizeof(sockaddr_in)); // 开启连接
    char wirteBuf[] = "client HelloWorld";
    write(client, wirteBuf, sizeof(wirteBuf));

    char readBuf[50];
    read(client, readBuf, 50);
    std::cout << readBuf << std::endl;
    shutdown(client, SHUT_RDWR);
    return 0;
}

二、服务器和客户端的信息获取