Socket套接字编程

【并发网络通信-套接字通信(C/C++ 多线程)】

套接字-Socket | 爱编程的大丙 (subingwen.cn)

字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。

目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian(大端和小端)

  • Little-Endian -> 主机字节序 (小端)

    • 数据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
    • 我们使用的PC机,数据的存储默认使用的是小端
  • Big-Endian -> 网络字节序 (大端)

    • 据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
    • 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。

1.转换函数

BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl​;从网络字节序到主机字节序的转换函数:ntohs、ntohl​。

#include <arpa/inet.h>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int

// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
uint16_t htons(uint16_t hostshort);
// 将一个整形从主机字节序 -> 网络字节序
uint32_t htonl(uint32_t hostlong);

// 将一个短整形从网络字节序 -> 主机字节序
uint16_t ntohs(uint16_t netshort)
// 将一个整形从网络字节序 -> 主机字节序
uint32_t ntohl(uint32_t netlong);

2.IP地址转换函数

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst); 
  • 参数

    • af​: 地址族(IP地址的家族包括ipv4和ipv6)协议

    • AF_INET​: ipv4格式的ip地址
    • AF_INET6​: ipv6格式的ip地址
    • src​: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
    • dst​: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
  • 返回值:成功返回1,失败返回0或者-1
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址      
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • 参数

    • af​: 地址族(IP地址的家族包括ipv4和ipv6)协议

    • AF_INET​: ipv4格式的ip地址
    • AF_INET6​: ipv6格式的ip地址
    • src​: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
    • dst​: 传出参数, 存储转换得到的小端的点分十进制的IP地址
  • 返回值:成功返回指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串。失败返回NULL

TCP通信流程

1.TCP通信特点

TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。

  • 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
  • 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
  • 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致

2.服务器端通信流程

服务器端先启动。

  1. 创建用于监听的套接字, 这个套接字是一个文件描述符

    int lfd = socket();
  2. 将得到的监听的文件描述符和本地的IP端口进行绑定

    bind();
  3. 设置监听(成功之后开始监听, 监听的是客户端的连接)

    listen();
  4. 等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的)没有新连接请求就阻塞

    int cfd = accept();
  5. 通信,读写操作默认都是阻塞的

    // 接收数据
    read(); / recv();
    // 发送数据
    write(); / send();
  6. 断开连接, 关闭套接字

    close();
    // 服务器端和客户端各调用一次close()

3.客户端通信流程

  1. 创建一个通信的套接字

    int cfd = socket();
  2. 连接服务器, 需要指定服务器的IP和端口

    connect();
  3. 通信

    // 接收数据
    read(); / recv();
    // 发送数据
    write(); / send();
  4. 断开连接, 关闭文件描述符(套接字)

    close();

套接字编程相关函数

1.socket()

man socket​:查看帮助文档

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

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

/*  参数
domain: 
    AF_LOCAL->本地通信
    AF_INET ->网络通信+IPv4
    AF_INET6->网络通信+IPv6

type:指定传输层协议
    SOCK_STREAM->流式传输协议,常用TCP
    SOCK_DGRAM ->报式传输协议,常用UDP

protocol: 
    在type的时候指定为SOCK_STREAM,在此处值为0,则说明使用的是TCP
    在type的时候指定为SOCK_DGRAM,在此处值为0,则说明使用的是UDP
*/
/*  返回值
成功:返回一个可以用于套接字通信的文件描述符。注意,这里的“通信”指既可以用于监听,也可以用于通信。
失败:返回-1
*/

2.bind()

man bind

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

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

/*  参数
sockfd: 用于监听的文件描述符,用socket()创建可得

addr:存储的本地IP和端口信息。这里的IP和端口必须是”大端“的(网络字节序)

addrlen: 前一个参数addr占用内存的大小
*/
/*  返回值
成功:返回0
失败:返回-1
*/

3.sockaddr数据结构

// 在写数据的时候不好用
struct sockaddr {
    sa_family_t sa_family;       // 地址族协议,只能是ipv4。因为IPv6所用的结构体不一样
    char        sa_data[14];     // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}

typedef unsigned short  uint16_t;
typedef unsigned int    uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

struct in_addr
{
    in_addr_t s_addr;
};  

// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
    sa_family_t sin_family;     /* 地址族协议: AF_INET */
    in_port_t sin_port;         /* 端口, 2字节-> 大端  */
    struct in_addr sin_addr;    /* IP地址, 4字节 -> 大端  */
    /* 填充 8字节 */
    unsigned char sin_zero[sizeof (struct sockaddr) - sizeof(sin_family) -
               sizeof (in_port_t) - sizeof (struct in_addr)];
};  

一般是先创建sockaddr_in​类型并进行数据的初始化,然后强制转换成sockaddr​类型。

因为两者占用内存大小是一样的,而sockaddr_in​使用起来更简单

4.listen()

man listen

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

int listen(int sockfd, int backlog);

/*  参数
sockfd: 用于监听的套接字,socket()创建并且用bind()进行了绑定

backlog:一次性能监听到多少个客户端的连接请求。最大值是128,128是在内存中写死了的。
    注意,backlog限制的是“一次性”能监听到的连接请求,不代表服务器能和多少个客户端建立连接。
*/
/*  返回值
成功:返回0
失败:返回-1
*/

5.accept()

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

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

// 没有客户端发送连接请求时,这个函数是阻塞的。每调用一次只能和一个客户端建立连接。
/*  参数
sockfd: 用于监听的套接字

addr:客户端的IP和端口信息。传出参数

addrlen:前一个参数addr所占用内存的大小。传入传出参数
*/
/*  返回值
成功:返回一个用于通信的文件描述符
失败:返回-1
*/

6.connect()

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

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

/*  参数
sockfd: 客户端创建的用于通信的套接字

addr:指定想要连接的服务器端的IP和端口信息。

addrlen:前一个参数addr所占用内存的大小。
*/
/*  返回值
成功:连接(connection)或绑定(binding)成功,则返回0。
失败:返回-1
*/
// 客户端的套接字也需要绑定,connect()会帮套接字随机绑定一个空闲端口

7.read()/recv()

// 接收数据。两个函数头文件不同
#include <unistd.h>
ssize_t read(int sockfd, void *buf, size_t size);

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t size, int flags);

/*  参数
sockfd: 用于通信的文件描述符, accept()函数的返回值

buf:指向一块有效内存, 用于存储接收的数据

size:参数buf指向的内存的容量。防止溢出

flags: 特殊的属性,一般不使用,指定为 0
*/
/*  返回值
大于0:实际接收的字节数
等于0:对方断开了连接
-1:接收数据失败了
*/

8.write()/send()

// 发送数据的函数。两个函数头文件不同
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t len);

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int fd, const void *buf, size_t len, int flags);

/*  参数
fd: 通信的文件描述符, accept() 函数的返回值

buf: 传入参数, 指向要发送的字符串

len: 要发送的字符串的长度

flags: 特殊的属性,一般不使用,指定为 0
*/
/*  返回值
大于0:实际发送的字节数,和参数len是相等的
-1:发送数据失败了
*/

9.close()

#include <unistd.h>

int close(int fd);

/*  参数
fd: 一个需要关闭的文件描述符(file descriptor)
*/
/*  返回值
成功:返回0
失败:返回-1
*/

文件描述符


在tcp的服务器端, 有两类文件描述符:

  • 监听的文件描述符

    • 只需要有一个
    • 不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
  • 通信的文件描述符

    • 负责和建立连接的客户端通信
    • 如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符

文件描述符在进行网络I/O操作时的对象:缓冲区

accept()​函数会检测监听文件描述符读缓冲区

  • 检测不到数据,该函数阻塞
  • 如果检测到数据,解除阻塞, 新的连接建立

write()/send()​函数会将数据写入通信文件描述符对应的写缓冲区中

  • 数据并没有被发送出去,只是被写入了缓冲区
  • 当写缓冲区写被写满时,这俩函数会阻塞
  • 内核检测到通信的文件描述符写缓冲区中有数据,,内核会将数据发送到网络中

read()/recv()​函数会检测通信文件描述符对应的读缓冲区

  • 检测不到数据,该函数阻塞
  • 如果检测到数据,解除阻塞, 并读出数据

服务器端的通信代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>    //包含了socket.h

int main()
{
    // 1.创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1){
        perror("socket");
        return -1;
    }

    // 2.绑定本地的IP和端口
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(32456);
    saddr.sin_addr.s_addr = INADDR_ANY; // 0=0.0.0.0。0不需要大小端转换。效果是,自动读取本地IP地址并绑定
    int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1){
        perror("bind");
        return -1;
    }

    // 3.设置监听
    ret = listen(fd, 128);
    if(ret == -1){
        perror("listen");
        return -1;
    }

    // 4.等待客户端连接
    struct sockaddr_in caddr;
    int addrlen = sizeof(caddr);
    int cfd = accept(fd, (struct sockaddr*)&caddr, &addrlen);
    if(cfd == -1){
        perror("accept");
        return -1;
    }

    // 连接建立成功,打印客户端的IP和端口信息
    char ip[24] = {0};
    printf("客户端IP:%s,端口:%d\n",
            inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ip, sizeof(ip)),
            ntohs(caddr.sin_port));

    // 5.通信
    while(1){
        //接收数据
        char buff[1024];
        int len = recv(cfd, buff, sizeof(buff), 0);
        if(len >0){
            printf("client say: %s\n",buff);
            send(cfd, buff, len, 0);
        }else if(len == 0){
            printf("客户端已断开连接...\n");
            break;
        }else{
            perror("recv");
            break;
        }
    }

    // 6.通信结束,关闭文件描述符
    close(fd);
    close(cfd);

    return 0;
}

客户端的通信代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>    //包含了socket.h

int main()
{
    // 1.创建通信的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1){
        perror("socket");
        return -1;
    }

    // 2.连接服务器IP, port
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(32456);
    inet_pton(AF_INET, "192.168.9.131", &saddr.sin_addr.s_addr);
    int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));
    if(ret == -1){
        perror("connect");
        return -1;
    }

    // 3.通信
    int number = 0;
    while(1){
        // 发送数据
        char buff[1024];
        sprintf(buff, "你好, hello world, %d...\n", number++);
        send(fd, buff, strlen(buff)+1, 0);

        // 接收数据
        memset(buff, 0, sizeof(buff));
        int len = recv(fd, buff, sizeof(buff), 0);
        if(len >0){
            printf("server say: %s\n",buff);
        }else if(len == 0){
            printf("服务器已断开连接...\n");
            break;
        }else{
            perror("recv");
            break;
        }
        sleep(1);
    }

    // 4.通信结束,关闭文件描述符
    close(fd);

    return 0;
}

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇