Socket编程基础

发布于:2025-05-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

这篇博客我们主要用来讲解TCP和UDP的socket编程。

概念

大多数网络协议都是由软件实现的(特别是协议栈中的高层协议),而且绝大多数计算机系统都将运输层以下的网络协议在操作系统的内核中进行实现。应用程序要想执行网络操作,必须通过操作系统为应用程序操作网络提供接口,这个接口通常称为网络应用编程接口(API)
虽然每个操作系统都可以自由地定义自己的网络API,但随着时间的推移,有些API获得了广泛的支持,例如,套接字(Socket)API。套接字API最初由加州大学伯克利分校的UNIX小组开发,现在几乎所有流行的操作系统都支持它。
套接字API定义了一组数据结构和操作。其中最重要的数据结构就是套接字数据结构(简称套接字),套接字是一个非常复杂的数据结构,包括进行网络操作所需的各种资源(如缓存)、各种参数(地址、端口号、协议类型等)。应用程序在进行网络操作前必须首先调用套接字API中定义的操作创建套接字数据结构,以获得进行网络操作所需的资源。由于套接字数据结构位于操作系统内核,因此应用程序不能直接访问该数据结构,需要通过创建操作返回的套接字描述符来操作该数据结构。此后,应用程序所进行的网络操作(建立连接、收发数据、调整网络通信参数等)都必须以该描述符为参数调用套接字API中的操作来完成。
在讲解编程之前。我们需要先学习几个概念,来帮助我们理解什么是套接字编程。

字节序

在各种计算机体系结构中,对于字节,字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正常的编/译码从而导致通信失败。

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

目前在各种体系的计算机中通常采用的字节存储机制主要有2种:Big-EndianLittle-Endian下面先从字节序说起。

  • Little-Endian(主机字节序,小端)
    数据的低位字节存储到内存中的低地址位,数据的高位字节存储到内存中的高地址位。我们使用的PC机,数据的存储默认使用的是小端。
  • Big-Endian(网络字节序,大端)
    数据的低位字节存储到内存中的高地址位,数据的高位字节存储到内存中的低地址位套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。
    我们来看一段代码
#include <stdio.h>
#include <arpa/inet.h>
int main()
{
    uint16_t host_num = 0x1234;
    uint16_t net_num = htons(host_num);
    printf("Host - ordered number: 0x%x\n", host_num);
    printf("Network - ordered number: 0x%x\n", net_num);
    return 0;
}

请添加图片描述

在大端序的主机系统中,host_numnet_num的值是相同的,因为主机字节序和网络字节序一致;而在小端序的主机系统中,host_numnet_num的值会不同,net_num会是host_num字节顺序转换后的结果。htons函数将会在下面进行讲解。从结果可以看出,我的机器是以小端方式存储数据的。
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。

接下来我们学习如何将主机字节序转换成网络字节序的各种API接口。需要对头文件是<arpa/inet.h>

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

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

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

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

IP地址转换

虽然IP地址本质是一个整型数,但是在使用的过程中都是通常一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换。需要对头文件是<arpa/inet.h>
函数原型:int inet_pton(int af, const char *src, void *dst);
参数:

  • af:地址族(IP地址的家族包括 IPV4 和 IPV6)协议。AF_INETIPV4格式的ip地址;AF_INET6IPV6格式的ip地址。
  • src:传入参数,对应要转换的点分十进制的ip地址,如:192.168.1.2
  • dst:传出参数,函数调用完成,转换得到的大端整型ip被写入到这块内存中
    返回值:成功返回1,失败返回0或者-1.

函数原型:const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
参数:

  • af:和上面的一样。
  • src:传入参数,这个指针指向的内存中存储了大端的整型IP地址
  • dst:传出参数,存储转换得到的小端的点分十进制的IP地址。
  • size:修饰dst参数的,标记dst指向的内存中最多可以存储多少个字节

返回值:

  • 成功:指针指向第三个参数对应的内存地址,通过返回值也可以直接取出转换得到的IP字符串。
  • 失败:返回NULL。

还有一组函数也能进行IP地址的大小端的转换,但是只能处理IPV4的IP地址。
函数原型:in_addr_t inet_addr(const char *cp);
功能:主要用于将一个用点分十进制表示的 IPv4 地址字符串(如 “192.168.1.1”)转换为网络字节序的 32 位二进制 IPv4 地址(in_addr_t类型,实际上是unsigned long int类型)。
参数:cp指向点分十进制IP地址字符串的指针。
如果转化失败,返回INADDR_NONE,本质上是数字-1
示例:

 int main()
{
    char *ip_str = "192.168.1.1";
    in_addr_t ip_num = inet_addr(ip_str);
    printf("The converted IP number is: 0x%lx\n", (long)ip_num);
    return 0;
}

请添加图片描述

该函数与inet_pton对照。


inet_ntoa用于将大端整型转换成点分十进制的IP。这也只能处理IPV4的地址
char* inet_ntoa(struct in_addr in)
示例:

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

int main(int argc, char *argv[])
{
    struct in_addr addr;
    if (argc != 2)
    {
        fprintf(stderr, "%s <dotted-address>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    if (inet_aton(argv[1], &addr) == 0)
    {
        fprintf(stderr, "Invalid address\n");
        exit(EXIT_FAILURE);
    }
    
    printf("%s\n", inet_ntoa(addr));
    exit(EXIT_SUCCESS);
}

请添加图片描述

该函数与inet_ntop对照。

sockaddr数据结构

sockaddr结构体定义在<sys/socket.h>头文件中。它是一种通用的套接字地址结构,用于在网络编程中表示各种类型的网络地址。
请添加图片描述

sockaddr
结构体定义:

struct sockaddr {
    sa_family_t sa_family;    /* 地址族,如AF_INET(IPv4)、AF_INET6(IPv6)等 */
    char        sa_data[14];  /* 包含具体的地址信息等,具体内容取决于sa_family */
};

sockaddr不能直接存储IPV4或者IPV6的信息。在实际使用的时候,我们通常会用到它的子类型,如sockaddr_in(IPV4)或者sockaddr_in6(IPV6),sockaddr_un用于域套接。
成员解释:

  • sa_family:这是一个地址族标识符。它用于指定地址的类型,例如AF_INET用于 IPv4 地址,AF_INET6用于 IPv6 地址,AF_UNIX用于本地(Unix 域)套接字通信等。
  • sa_data:这是一个字符数组,用于存储具体的地址信息。其内容和长度取决于sa_family的值。对于 IPv4 地址,实际上只使用了其中的一部分来存储 IP 地址(4 字节)和端口号(2 字节)等信息;对于 IPv6 地址,会使用更多的字节来存储完整的 128 位 IPv6 地址和端口号等。不过,由于sa_data的使用方式比较复杂且不直观,在实际编程中通常不直接使用这个成员,而是使用针对具体地址族的派生结构体(如sockaddr_insockaddr_in6)。

对于 IPv4 地址,通常使用sockaddr_in结构体,其定义如下:

struct sockaddr_in {
    sa_family_t    sin_family; /* 地址族,对于IPv4为AF_INET */
    in_port_t      sin_port;   /* 端口号,网络字节序 */
    struct in_addr sin_addr;   /* IPv4地址结构体 */
    char           sin_zero[8];/* 填充字节,保证结构体大小和sockaddr相同 */
};

其中sin_family明确指定为AF_INETsin_port存储端口号(使用htons函数将主机字节序转换为网络字节序),sin_addr是一个struct in_addr类型的结构体,用于存储 IPv4 地址(可以使用inet_addrinet_pton函数来设置)。
其中sin_addr的类型是struct in_addr,我们来看一下这个结构体。

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

此处的in_addr_t是一个32位的整型,使用的时候,要以这种方式进行使用,嵌套了两层结构体。

struct sockaddr_in local;
local.sin_addr.s_addr = xxx

对于IPV6的使用,在以后需要使用的时候,在进行讲解。好了,预备知识准备完了,接下来,我们先来学习UDP的socket编程。

UDP Socket编程

使用UDP进行通信无须在客户端和服务器端之间建立连接,流程比TCP要简单一些,基本流程如下。
请添加图片描述

socket
功能:用于创建一个套接字。头文件<sys/socket.h>
函数原型:int socket(int domain, int type, int protocol);
参数:

  • domain:协议族,用于指定通信的域。常见的有AF_INETAF_INET6AF_UNIX
  • type:用于指定套接字的类型。主要有SOCK_STREAM,提供面向连接的、可靠的、字节流服务,通常与TCP协议一起使用。SOCK_DGRAM,提供无连接的、不可靠的数据包服务,一般与UDP协议使用。
  • protocol:用于指定使用的具体协议,一般设置为0,系统会自动选择合适的协议。
    返回值:如果创建成功,sockfd会得到一个非负的套接字描述符;失败返回-1,设置错误码。
    示例:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

bind
功能:主要用于将一个套接字(由sockfd指定)与一个本地网络地址(由addr指定)绑定在一起。这就好比给一个电话听筒(套接字)分配一个电话号码(网络地址),这样其他设备才能通过这个地址找到并与之通信。
函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:

  • sockfd:通过socket创建的套接字描述符,这个套接字必须是有效的。
  • addr:这是一个指向sockaddr的结构体或者其他派生结构体。
  • addrlen:这是一个socklen_t类型的变量,表示addr所指向的结构体的长度。
    返回值:成功返回0;失败返回-1,设置错误码。

示例:

int main()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("Socket creation error");
        return -1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        perror("Bind error");
        return -1;
    }
    // 后续可以进行listen、accept等操作
    close(sockfd);
    return 0;
}

首先创建了一个UDP(SOCK_DGRAM)套接字sockfd,然后初始化sockaddr_in结构体server_addr,并设置其成员变量。最后通过bind()函数将套接字sockfdserver_addr绑定。


recvfrom
功能:用于从指定的套接字(sockfd)接收数据。这个函数通常用于无连接(UDP)的套接字通信。头文件<sys/types.h><sys/socket.h>
函数原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:

  • sockfd:套接字描述符。
  • buf;指向用户分配的缓冲区指针。接收的数据将会被存储到这个缓冲区中。
  • len;指定缓冲区buf的最大长度,用于限制接收数据的数量,单位是字节。
  • flags:接收数据的标志位,通常设置为0。
  • src_addr:指向struct sockaddr类型的指针。对于 UDP 套接字,当接收到数据时,这个结构体会被填充发送者的地址信息(包括 IP 地址和端口号等)。
  • addrlen:是一个指向socklen_t类型(通常是size_t类型)的指针。在调用recvfrom()函数之前,这个指针所指向的值应该是src_addr结构体的长度;在函数返回后,这个值会被更新为实际发送者地址结构体的长度。

返回值:成功时返回实际读取的字节数;失败返回-1,设置错误码。


sendto
功能:主要用于在套接字(sockfd)上发送数据。它通常用于无连接的套接字通信,如 UDP(用户数据报协议)通信。
函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数:

  • sockfd:已经创建的有效套接字描述符,用于发送数据的通道。
  • buf:指向包含要发送数据的缓冲区指针。这个缓冲区中的数据将被发送出去
  • len;指定了要从缓冲区buf中发送的数据的长度,单位是字节。
  • flags;控制发送方式都标志位。一般设置为0.
  • dest_addr:指向struct sockaddr类型的指针,包含了目标接收方的地址信息(如IP地址和端口号等)
  • addrlen:指向socklen_t类型的指针,表示dest_addr结构体的长度。

返回值:成功时返回发送的字节数,失败时返回-1,设置错误码;返回0表示没有读取到数据。
示例:这段代码是使用UDP协议的客户端程序向服务端发送信息的过程。

#define BUFFER_SIZE 1024
int main()
{
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("Socket creation error");
        return -1;
    }
    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("127.0.0.1");
    server_addr.sin_port = htons(8080);
    char buffer[BUFFER_SIZE];
    strcpy(buffer, "Hello, Server!");
    socklen_t server_addr_len = sizeof(server_addr);
    ssize_t num_bytes = sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, server_addr_len);
    if (num_bytes < 0)
    {
        perror("Sendto error");
        return -1;
    }
    printf("Sent %ld bytes to the server.\n", (long)num_bytes);
    close(sockfd);
    return 0;
}

请添加图片描述

首先创建了一个 UDP 套接字sockfd,然后初始化了服务器端的地址server_addr,包括设置地址族、IP 地址和端口号。接着,将一个字符串复制到缓冲区buffer中,最后使用sendto()函数将缓冲区中的数据发送到服务器端。如果发送成功,会打印出发送的数据字节数。

当我们作为客户端向服务端发送信息的时候,并没有使用bind()函数,难道说作为客户端就不用bind吗?当然还是要bind的,只不过不需要用户显示的bind!一般有OS自由随机选择!在实际开发的过程中,服务端一般是有指定的端口号的,这样客户端才知道向哪一个服务端发送请求,所以服务端需要bind端口号,不能让操作系统进行分配,而客户端不在意端口号,只要能与服务端取得通信即可,所以客户端不需要bind,只要能保证主机上的唯一性就可以!那么系统什么时候会给客户端bind呢?首次发送数据的时候就进行bind了。简单来说,一个端口号只能被一个程序bind,而一个程序可以bind多个端口号。


bzero
在上面的程序中,我们使用的memset函数来初始化sockaddr_in结构体的成员变量变成0。这是C语言的用法,我们还有一个函数也可以进行初始化,就是bzero,它的头文件是<strings.h>
功能:用于将内存块中的前n个字节设置为零。这里的内存块是由指针s指向的。
函数原型:void bzero(void* s, size_t n);
参数:

  • s:要初始化的内存地址。
  • n:需要初始化的字节数。
    bzero()在功能上更专注于将内存区域清零,它将字节设置为零,而memset()可以将内存区域设置为任意指定的值(通过参数c指定)。

示例:

int main()
{
    char str[10];
    bzero(str, sizeof(str));
    printf("%s\n", str);
    return 0;
}

该代码结果会输出一个空字符串。

TCP Socket编程

TCP的套接字编程流程如下。
请添加图片描述

我们可以把TCP套接字比喻成电话机,下面我将通过电话机讲解套接字的创建及使用方法。电话机可以同时用来拨打或接听,但对套接字而言,拨打和接听是有区别的。我们先讨论用于接听的套接字创建过程。

  1. 调用socket函数(安装电话机)时进行的对话。

问:“接电话需要准备什么?”
答:“当然是电话机!”

有了电话机才能安装电话,所以socket函数就相当于电话机的套接字。接下来,我们只需要购买机器,剩下的安装和分配电话号码等工作都由电信局的工作人员完成。而套接字需要我们自己安装,这也是套接字编程的难点所在,,但多安装几次就会发现其实不难。准备好电话机后要考虑分配电话号码的问题,这样别人才能联系到自己。
2. 调用bind函数(分配电话号码)时进行的对话

问:“请问您的电话号码是多少?”
答:“我的电话号码是123-1234。”

套接字同样如此。就像给电话机分配电话号码一样(虽然不是真的把电话号码给了电话机),利用bind函数就可以给创建好的套接字分配地址信息(IP地址和端口号)。调用bind函数给套接字分配地址后,就基本完成了接电话的所有准备工作。接下来需要连接电话线并等待来电。
3. 调用listen函数(连接电话线)时进行的对话

问:“已架设完电话机后是否只需要连接电话线?”
答:“对,只需连接就能接听电话。”

一连接电话线,电话机就转为可接听状态,这时其他人可以拨打电话请求连接到该机。同样,需要把套接字转化成可接收连接的状态。
连接好电话线后,如果有人拨打电话就会响铃,拿起话筒才能接听电话。这时就要用到accept函数。
4. 调用accept函数(拿起话筒)时进行的对话

问:“电话铃响了,我该怎么办?”
答:“难道您真不知道?接听啊!”

拿起话筒意味着接收了对方的连接请求。如果有人为了完成数据传输而请求连接,就需要调用accept函数进行受理。
要想完成TCP的套接字编程,还需要学习几个接口函数,才能完全的掌握TCP的服务流程。

listen:进入等待连接请求状态
假如我们已调用bind函数给套接字分配了地址,接下来就要通过调用listen函数进入等待连接请求状态。只有调用了listen函数,客户端才能进入可发出连接请求的状态。换言之,这时客户端才能调用connect函数(若提前调用将发生错误)。
头文件:<sys/socket.h><sys/types.h>
函数原型:int listen(int sockfd, int backlog);
参数:

  • sockfd:希望进入等待连接请求状态的套接字文件描述符。
  • backlog:连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。

返回值:成功时返回0,失败返回-1。
先解释一下等待连接请求状态的含义和连接请求等待队列。“服务器端处于等待连接请求状态”是指,客户端请求连接时,受理连接前一直使请求处于等待状态。如图所示:
请添加图片描述

服务端套接字是接收连接请求的一名门卫或一扇门。客户端如果向服务端询问:“请问我是否可以发起连接?”服务器端套接字就会亲切的应答:“您好!当然可以,但系统正忙,请到等候室排号等待,准备好后会立即受理您的连接。”同时将连接请求到等候室。调用listen函数即可生成这种门卫(服务端套接字),listen函数的第二个参数决定了等候室的大小。等候室称为连接请求等待队列,准备好服务端套接字和连接请求等待队列后,这种可接收连接请求的状态称为等待连接请求状态。


accept:受理客户端连接请求
头文件:<sys/socket.h><sys/types.h>
函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:

  • sockfd:服务器套接字的文件描述符。
  • addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
  • addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。

返回值:成功时返回创建的套接字文件描述符,失败返回-1。
accept函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。需要强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。如图所示展示了accept函数调用过程。
如果调用accept函数时若等待队列为空,则accept函数不会返回,直到队列中出现新的客户端连接
请添加图片描述

图中展示了“从等待队列中取出1个连接请求,创建套接字并完成连接请求”的过程。服务器端单独创建的套接字与客户端建立连接后进行数据交换。


recv:用于从套接字接收数据
头文件:<sys/socket.h>
函数原型:ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)
参数:

  • sockfd:表示数据接收对象的连接的套接字文件描述符。
  • buf:保存接收数据的缓冲器地址。
  • nbyters:可接收端最大字节数。
  • flags:接收数据时指定的可选项信息。

返回值:成功时返回接收的字节数(收到EOF时返回0),失败时返回-1.
send函数和recv函数的最后一个参数是收发数据时的可选项。该可选项可以利用|运算符同时传递多个信息。

可选项 含义 send recv
MSG_OOB 用于传输带外数据
MSG_PEEK 验证输入缓冲是否存在接收端数据
MSG_DONTROUTE 数据传输过程中不参照路由表,在本地网络中寻找目的地
MSG_DONTWAIT 调用I/O函数时不阻塞,用于使用非阻塞I/O
MSG_WAITALL 防止函数返回,直接接收全部请求的字节数

实现基于TCP的服务端

下图是TCP服务器端默认的函数调用顺序,绝大部分TCP服务器端都按照该顺序调用。
请添加图片描述

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
  
void error_handling(char *message);
  
int main(int argc, char *argv[])
{
    int serv_sock; // 服务端网络文件描述符
    int clnt_sock; // 客户端网络文件描述符

    struct sockaddr_in serv_addr; // 服务端地址信息
    struct sockaddr_in clnt_addr; // 客户端地址信息
    socklen_t clnt_addr_size;
  
    char message[] = "hello world!";
    if (argc != 2)
    {
        printf("Usage:%s <port>\n", argv[0]);
        exit(1);
    }
  
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
    {
        error_handling("socket() err");
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
  
    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");
  
    write(clnt_sock, message, sizeof(message));
    close(serv_sock);
    close(clnt_sock);

    return 0;
}

void error_handling(char *msessage)
{
    fputs(msessage, stderr);
    fputc('\n', stderr);
    exit(1);
}

服务器端实现过程中要先创建套接字,通过socket创建套接字,但此时的套接字尚非真正的服务器端套接字,接着为了完成套接字的地址分配,初始化结构体变量并调用bind函数。调用listen函数进入等待连接请求状态。连接请求等待队列的长度设置为5,此时的套接字才是服务器端套接字;然后调用accept函数从队头取1个连接请求与客户端建立连接,并返回创建的套接字文件描述符。最后调用write函数向客户端传输数据,调用close函数关闭连接。

实现基于TCP的客户端

客户端调用接口的过程如下。
请添加图片描述

与服务器端相比,区别就在于“请求连接”,它是创建客户端套接字后服务器端发起的连接请求。服务器端调用listen函数后创建连接请求等待队列,之后客户端即可请求连接。那如果发起连接请求呢?调用connect函数即可完成。
头文件:<sys/types.h><sys/socket.h>
函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:

  • sockfd:客户端套接字文件描述符。
  • addr:保存目标服务器端地址信息的变量地址值。
  • addrlen:以字节为单位传递已传递给第二个结构体参数addr的地址变量长度。

返回值:成功返回0,失败返回-1.
客户端调用connect函数后,发生以下情况之一才会返回(完成函数调用)

  • 服务器端接收连接请求。
  • 发生断网等异常情况而中断连接请求。

所谓的“接收连接”并不意味着服务器端调用accept函数,其实是服务器端把连接请求信息记录到等待队列。因此connect函数返回后并不立即进行数据交换。
在客户端实现的过程中并没有出现套接字地址的分配,而是创建套接字后立即调用connect函数。难道客户端套接字无需分配IP和端口吗?当然不是!网络数据交换必须分配IP和端口。既然如此,那么客户端套接字何时、何地、如何分配地址呢?

  • 何时?调用connect函数时。
  • 何地?操作系统,准确来说是在内核当中。
  • 如何?IP用计算机(主机)的IP,端口随机。

客户端的IP地址和端口在调用connect函数时自动分配,无需调用标记的bind函数进行分配。
客户端代码:

// 头文件和服务端一样
  
void error_handling(char *message);
int main(int argc, char *argv[])
{
    int serve_sock_fd; // 服务器端套接字
    struct sockaddr_in serv_addr;
    char message[30];
    int str_len;

    if (argc != 3)
    {
        printf("Usage:%s <IP> <port>\n", argv[0]);
        exit(1);
    }

    serve_sock_fd = socket(PF_INET, SOCK_STREAM, 0);
    if (serve_sock_fd == -1)
        error_handling("socket() error");
  
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if (connect(serve_sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");

    str_len = read(serve_sock_fd, message, sizeof(message) - 1);
    if (str_len == -1)
        error_handling("read() error");
    printf("Message from server:%s\n", message);
    close(serve_sock_fd);

    return 0;
}

void error_handling(char *msessage)
{
    fputs(msessage, stderr);
    fputc('\n', stderr);
    exit(1);
}

请添加图片描述


网站公告

今日签到

点亮在社区的每一天
去签到