当前位置:   article > 正文

socket简介和客户端与服务器通信小案例_socket服务器

socket服务器

socket介绍(IP地址,端口号)

所谓 socket(套接字),就是对(网络中不同主机上的应用进程之间进行双向通信的)端点 的抽象。

一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。

从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。

**socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。**它是网络环境中进程间通信的 API,也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket 通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段信息。

socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

socket 本身有“插座”的意思,在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。**本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。**与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

套接字通信分两部分:

  • 服务器端:被动接受连接,一般不会主动发起连接;
  • 客户端:主动向服务器发起连接;

socket是一套通信的接口,Linux 和 Windows 都有,但是有一些细微的差别。

字节序

概念:(大端字节序 和 小端字节序)
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。

字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian):

大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;(高字节在低位)
小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。(低字节在高位)
在这里插入图片描述
如何验证自己电脑上是大端还是小端?

// 通过代码检测当前主机的字节序
#include <stdio.h>

int main() {

    union {
        short value;    // 2字节
        char bytes[sizeof(short)];  // char[2]
    } test;

    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
        printf("大端字节序\n");
    } else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {
        printf("小端字节序\n");
    } else {
        printf("未知\n");
    }

    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

字节序转换函数(网络字节序-大、主机字节序-小)

网络字节顺序是 TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序默认采用大端排序方式。

主机字节序一般是小端排序方式

从主机字节序到网络字节序的转换函数:htons、htonl;
从网络字节序到主机字节序的转换函数:ntohs、ntohl。

h - host 主机,主机字节序
to - 转换成什么
n - network 网络字节序
s - short, unsigned short(两个字节:端口号)
l - long, unsigned int(四个字节:IP地址)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数原型:

#include <arpa/inet.h>
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 - 网络字节序
uint16_t ntohs(uint16_t netshort); // 网络字节序 - 主机字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 - 网络字节序
uint32_t ntohl(uint32_t netlong); // 网络字节序 - 主机字节序

typedef unsigned short uint16_t;//16位无符号整型
typedef unsigned int uint32_t;//32位无符号整型

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

示例:
示例1:htons()函数、htonl()函数、ntohl()函数

#include <stdio.h>
#include <arpa/inet.h>

int main() {

    //printf("a = %d, b = %d\n", sizeof(unsigned short), sizeof(unsigned short int)); // a = 2, b = 2
    // htons 转换端口(short类型,占2个字节)
    unsigned short a = 0x1234;//主机字节序(小端模式)
    printf("a : %x\n", a);
    unsigned short b = htons(a);//转换成网络字节序(大端模式)
    printf("b : %x\n", b);

    printf("=======================\n");

    // htonl 转换IP(int类型,占4个字节)
    char buf[4] = {100, 80, 1, 100};
    int num = *(int *)buf;
    printf("num = %d\n", num);
    int sum = htonl(num);
    unsigned char *p = (unsigned char *)&sum;

    printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));

    printf("=======================\n");

    // ntohl
    unsigned char buf1[4] = {1, 1, 80, 100};
    int num1 = *(int *)buf1;
    int sum1 = ntohl(num1);
    unsigned char *p1 = (unsigned char *)&sum1;
    printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
    
     // ntohs


    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

结果:

a : 1234
b : 3412
=======================
num = 1677807716
100 1 80 100
=======================
100 80 1 1

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

示例2:htons()函数

1.把主机字节序转换成网络字节序:
htons(8800);//端口号

  • 1
  • 2
  • 3

socket地址

socket地址其实是一个结构体,封装端口号和IP地址等信息。后面的socket相关的api中需要使用到这个socket地址。

通用socket地址(结构体sockaddr)

socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:

#include <bits/socket.h>
struct sockaddr {
	sa_family_t sa_family;
	char sa_data[14];
};
typedef unsigned short int sa_family_t;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

参数1:
其中sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain)和对应的地址族入下所示:

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

参数2:
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:

协议族地址值含义和长度
PF_UNIX文件的路径名,长度可达到108字节
PF_INET16 bit 端口号和 32 bit IPv4 地址,共 6 字节
PF_INET616 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节

由上表可知,**14 字节的 sa_data 根本无法容纳多数协议族的地址值。**因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。

#include <bits/socket.h>
struct sockaddr_storage
{
	sa_family_t sa_family;
	unsigned long int __ss_align;
	char __ss_padding[ 128 - sizeof(__ss_align) ];
};
typedef unsigned short int sa_family_t;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

专用socket地址

(结构体sockaddr_in 和 结构体sockaddr_in6)
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
在这里插入图片描述
注意:
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。

1.UNIX 本地域协议族使用如下专用的 socket 地址结构体:

#include <sys/un.h>
struct sockaddr_un
{
	sa_family_t sin_family;
	char sun_path[108];
};

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6:

//1.头文件:
#include <netinet/in.h>

//2.IPv4:
struct in_addr//IPv4地址结构体
{
	in_addr_t s_addr;
};
struct sockaddr_in
{
	sa_family_t sin_family; //地址族(AF_INET)
	in_port_t sin_port; //端口号(2字节,16位)
	struct in_addr sin_addr;//IPv4地址(4字节,32位)
	//补全到sockaddr结构体中char sa_data[14]的大小,即14个字节
	unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
	sizeof (in_port_t) - sizeof (struct in_addr)];
};

//3.IPv6:
struct sockaddr_in6
{
	sa_family_t sin6_family;
	in_port_t sin6_port; /* Transport layer port # */
	uint32_t sin6_flowinfo; /* IPv6 flow information */
	struct in6_addr sin6_addr; /* IPv6 address */
	uint32_t sin6_scope_id; /* IPv6 scope-id */
};

typedef unsigned short uint16_t;//16位无符号整型
typedef unsigned int uint32_t;//32位无符号整型
typedef uint16_t in_port_t;//端口号
typedef uint32_t in_addr_t;//IP地址
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

P地址转换(字符串ip-整数 ,主机、网络字节序的转换)

通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。

下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);

  • 1
  • 2
  • 3
  • 4
  • 5

下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
①inet_pton()函数:

#include <arpa/inet.h>
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
//convert IPv4 and IPv6 addresses from text to binary form

  • 1
  • 2
  • 3
  • 4
  • 5

参数:
af:地址族: AF_INET 或者 AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
②inet_ntop()函数:

// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//convert IPv4 and IPv6 addresses from binary to text form

  • 1
  • 2
  • 3
  • 4

af:地址族: AF_INET 或者 AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的

TCP

TCP通信流程

在这里插入图片描述
TCP 通信的流程:
服务器端 (被动接受连接的角色)

  1. 创建一个用于监听的套接字 —socket()函数
    监听:监听是否有客户端的连接
    套接字:这个套接字其实就是一个文件描述符(fd)
  2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息,客户端连接服务器的时候使用的就是这个IP和端口) — bind()函数
  3. 设置监听,监听的fd开始工作 — listen()函数
  4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)— accept()函数
  5. 通信
    接收数据 — recv()函数/read()函数
    发送数据 — send()函数/write()函数
    通信结束,断开连接 —close()函数

客户端(主动连接):

  1. 创建一个用于通信的套接字(fd)—socket()函数
  2. 连接服务器,需要指定连接的服务器的 IP 和 端口 — connect()函数
  3. 连接成功了,客户端可以直接和服务器通信
    接收数据 — recv()函数/read()函数
    发送数据 — send()函数/write()函数
  4. 通信结束,断开连接 — close()函数

套接字函数
头文件:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略

  • 1
  • 2
  • 3
  • 4

socket()函数:

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

  • 1
  • 2

功能:创建一个套接字
参数:

  • domain: 协议族
    AF_INET : ipv4
    AF_INET6 : ipv6
    AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
  • type: 通信过程中使用的协议类型
    SOCK_STREAM : 流式协议(流stream)
    SOCK_DGRAM : 报式协议(数据报datagram)
  • protocol : 具体的一个协议,一般写0
    SOCK_STREAM 默认使用 TCP
    SOCK_DGRAM 默认使用 UDP
    返回值:

成功:返回文件描述符fd,操作的就是内核缓冲区。
失败:-1

bind()函数:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命名

  • 1
  • 2

功能:绑定,将fd 和本地的 IP + 端口 进行绑定
参数:

  • sockfd : 通过socket()函数得到的文件描述符;
  • addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息,需强制转化为通用 socket 地址类型 sockaddr;
  • addrlen : 第二个参数结构体占的内存大小。

listen()函数:

int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn

  • 1
  • 2

功能:监听这个socket上的连接
参数:

  • sockfd : 通过socket()函数得到的文件描述符;
  • backlog : (未连接的和已经连接的)和的最大值,比如5。

accept()函数:(阻塞)

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

  • 1
  • 2

功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
参数:

  • sockfd : 用于监听的文件描述符;
  • addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port);
  • addrlen : 指定第二个参数的对应的内存大小。

返回值:

  • 成功 :用于通信的文件描述符fd,后面的send()、recv()、write()、read()等函数就是用这个来传送信息;
  • -1 : 失败

connect()函数:

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

功能: 客户端连接服务器
参数:

  • sockfd : 用于通信的文件描述符
  • addr : 客户端要连接的服务器的地址信息(ip,port)
  • addrlen : 第二个参数的内存大小

返回值:成功 0, 失败 -1

write()函数、read()函数

ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据

  • 1
  • 2
  • 3

☆服务器通信小案例

server.c

// TCP 通信的服务器端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建socket(用于监听的套接字)
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "192.168.193.128", saddr.sin_addr.s_addr);
    saddr.sin_addr.s_addr = INADDR_ANY;  // 0.0.0.0
    saddr.sin_port = htons(9999);
    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.接收客户端连接
    struct sockaddr_in clientaddr;
    int len = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &len);
    
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    // 输出客户端的信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);
    printf("client ip is %s, port is %d\n", clientIP, clientPort);

    // 5.通信
    char recvBuf[1024] = {0};
    while(1) {
        
        // 获取客户端的数据
        int num = read(cfd, recvBuf, sizeof(recvBuf));
        if(num == -1) {
            perror("read");
            exit(-1);
        } else if(num > 0) {
            printf("recv client data : %s\n", recvBuf);
        } else if(num == 0) {
            // 表示客户端断开连接
            printf("clinet closed...");
            break;
        }

        char * data = "hello,i am server";
        // 给客户端发送数据
        write(cfd, data, strlen(data));
        sleep(3);
    }
   
    // 关闭文件描述符
    close(cfd);
    close(lfd);

    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

client.c

// TCP通信的客户端

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

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

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.0.16.2", &serveraddr.sin_addr.s_addr);//124.221.96.249
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    
    // 3. 通信
    char recvBuf[1024] = {0};
    while(1) {

        char * data = "hello,i am client";
        // 给客户端发送数据
        write(fd, data , strlen(data));

        sleep(3);
        
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server data : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

    }

    // 关闭连接
    close(fd);

    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/622894
推荐阅读
相关标签
  

闽ICP备14008679号