网络编程学习笔记-基础篇

前言

大家好,我是雨墨,昨天发了我学习网络编程的笔记,但是当我学到后面回顾的时候,发现自己并未对前面的内容有很深入的掌握,于是暂缓学习的脚步,跟着极客时间网络编程实战专栏的盛延敏老师回顾了下前面学习的知识,于是我将自己的笔记重新整理并发布,希望对大家有所帮助。如果对你有帮助,点个赞或收藏再走吧,后续我也会持续更新我的笔记~
这个专栏可以说是把我前面的疑惑扫除干净了,在此申明我不是托,文章中有些内容转自极客时间专栏,我已给出链接。如有侵权,请联系我速删!

1、理解网络编程和套接字

linux 头文件 #include <sys/socket.h>

基于linux平台的实现

1.1 什么是 socket?

img

​这幅图非常重要,转自极客时间网络编程专栏

在网络编程中,客户端和服务器是工作的核心逻辑。

服务器端:首先初始化 socket ,然后通过 bind 将创建好的套接字的服务能力绑定到一个 IP 地址和端口上,然后执行 listen 操作,将原先得到的 socket 转化为服务端的 socket ,最后阻塞在 accept 等待客户端的请求到来。

客户端:先创建好 socket ,再执行 connect 向预先知晓的服务器 IP 地址和端口号发出请求,这个过程就是 TCP 三次握手

接下来进行数据传输过程,即 “读”“写”,客户端通过向操作系统内核发出 write 字节流写操作,内核协议栈将字节流通过网络设备发送到服务器端,服务器端从内核协议栈中通过 read 字节流读操作读入信息到进程中,开始业务逻辑的处理,完成业务之后,将处理的信息重新通过 “写” 操作返回给客户端,而客户端也通过 “读” 操作将信息收取。可以看到,数据的传输是双向的。

当完成数据的传输之后,客户端不想与服务器端进行交互了(服务器也可断开连接),就需要调用 close func ,客户端的操作系统内核就会通过原先建立的链路向服务器端发送 FIN 包,这就涉及 TCP 四次挥手的内容了,服务器端收到客户端发送的 FIN 包之后,处于被动关闭的状态,但是服务器会继续发送没有发完的数据,直到服务器端也执行了 close 操作之后,整个链路才会真正的关闭。

由上图可见,所有的操作都是依靠 socket 来完成的!

服务器端

1. 调用socket函数创建套接字	// 建立电话线
int socket(int domain,int type ,int protocol);
int serv_sock(PF_INET, SOCK_STREAM, 0);

2. 调用bind函数分配IP地址和端口号	// 给你一个手机号
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
if (bind(serv_sock, (struct sockaddr)&serv_addr, sizoef serv_addr) == -1)
    ...
    
3. 调用listen函数转化为可接收请求状态	// 在电话机前等
int listen(int sockfd, int backlog);	// backlog 表示消息队列的长度,自己设定
if (listen(serv_sock, 5) == -1)
    ...
    
4. 调用accept函数受理连接请求	// 接电话
int accept(int sockfd, struct sockaddr *addr , socklen_t *addrlen);	// 创建 client sockfd
socklen_t clnt_addr_sz  = sizeof clnt_addr;
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_sz)

客户端

1、调用 socket 函数创建套接字
int socket(int domain,int type ,int protocol);
int sock = socket(PF_INET, SOCK_SRTREAM, 0);

2、调用 connect 函数向服务器发出请求       
int connect(int socketfd, struct sockadd* serv_addr, socklen_t addrlen);
if (connect(sock, (struct sockaddr*)&serv_addr, sizeof serv_addr) == -1)
    ...

形容 socket 的一个很棒的例子:

socket 好像家里的电话机,bind 就像是电信公司给我们开的户,也就是本地电话号码,当有人通过 connect 拨号过来找你的时候, listen 就像家里的人听到了电话响,accept 就像家里的人拿起电话应答。

然后,对方说的话就写进了 write ,而接听电话的听就像是 read ,接下来该接听方说话了,说的话进入了 write ,然后该该拨号方 read ,双方就这样进行了交互,这个交互是双向的。

最终,拨号方挂了电话,接听方听到对方挂了电话也相继挂上电话,至此连接断开。

1.2 套接字地址格式

在使用套接字时,首先就要解决通信双方寻址的问题,这就要使用套接字的地址建立连接。

通用套接字地址格式
/* POSIX.1g 规范规定了地址族为2字节的值.  */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址  */
struct sockaddr{
    sa_family_t sa_family;  /* 地址族.  16-bit*/
    char sa_data[14];   /* 具体的地址值 112-bit */
  }; 

sockaddr 的第一个字段表示地址族,常用的地址族有:

  • PF_INET IPv4互联网协议族
  • PF_INET6 IPv6
  • PF_LOCOL 本地通信的UNIX协议族
IPV4 套接字地址格式
struct sockaddr_in
{
    sa_family_t sin_family;  // 地址族(Address Family),对应于 AF_INET
    uint16_t sin_port;       // 16 位 TCP/UDP 端口号,以网络字节序保存
    struct in_addr sin_addr; // 32位 IP 地址
    char sin_zero[8];        // 不使用,必须填充为0,使sockaddr_in和sockadd结构体保持一致
};

该结构体中提到的另一个结构体 in_addr 定义如下,它用来存放 32 位IP地址

struct in_addr
{
    in_addr_t s_addr; //32位IPV4地址
}

端口号是在同一操作系统内区分不同套接字而设置的。不能将同一端口号分给不同套接字,但是 TCP 和 UDP 不会共用端口号,所以允许UDP和TCP使用同一端口号

大于 5000 的端口可以作为我们自己应用程序的端口使用,介绍几个常用的熟知端口号:

FTP :21
SSH:22
HTTP:80

IPV6 套接字格式
struct sockaddr_in6
  {
    sa_family_t sin6_family; /* 16-bit */
    in_port_t sin6_port;  /* 传输端口号 # 16-bit */
    uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
    struct in6_addr sin6_addr;  /* IPv6地址128-bit */
    uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
  };
本地套接字格式
struct sockaddr_un {
    unsigned short sun_family; /* 固定为 AF_LOCAL */
    char sun_path[108];   /* 路径名 */
};

1.3 通过套接字建立连接

创建套接字
int socket(int domain, int type ,int protocol)

domain : 套接字中使用的协议族信息,本书采用 ipv4 ,所以使用 PF_INET,PF_ 的意思是 Protocol Family
type: 套接字数据传输类型信息,按照面向连接传输分为 SOCK_STREAM ,按照面向数据从传输分为 SOCK_DGRAM,表示原始套接字用 SOCK_RAW
protocol: 计算机间通信使用的协议信息,由于面向连接传输且在 ipv4 中的是 IPPROTO_TCP ,所以根据前面两个信息可以推出它来,可以直接写 0 。同理 IPPROTO_UDP 也可直接用 0 表示。

套接字类型(type):套接字的数据传输方式

  1. 面向连接的套接字(SOCK_STREAM)

    特征:可靠,按序基于字节的面向连接(一对一)的数据传输方式的套接字 ,想象为传送带上传送东西,socket 相当于站在两个车间的工人。

  2. 面向消息的的套接字(SOCK_DGRAM)

    特征: 不可靠,不按序,以数据的高速传输为目的的套接字,可以想象成骑着摩托车送快递的小哥,快递送的很快,但是容易坏,并且每次只能送那么多。

bind:设定电话号码

如果需要让别人可以拨这个号码,就要去电信公司注册这张电话卡,同理,如果需要创建出的套接字能被别人使用,就要使用 bind 函数把该套接字与套接字地址绑定。

bind(int fd, sockaddr * addr, socklen_t len)

后续的使用中,传入 bind 的参数可能是 IPV4、IPV6、本地套接字格式,bind 函数能够通过 len 判断如何解析 addr 。

其实可以将 bind 函数理解为:

bind(int fd, void* addr, socklen_t len)

这是因为 BSD 在设置套接字的时候 C 语言还没有推出 void* ,聪明的 BSD 的设计者想到了使用通用地址格式来支持 bind 和 accept 等这些函数的参数。

我们把地址设置为本地的 IP 地址,相当于告诉操作系统内核,只对目的 IP 是本地 IP 的数据包进行处理,这样写的程序在部署的时候会存在问题,因为我们并不知道自己的应用程序最终会部署到哪台机器上,这是可以使用通配地址。这就相当于告诉操作系统内核,只要目的地址是本地的 IP 地址就行,因为有些机器可能有多块网卡,服务器一般使用通配地址(INADDR_ANY)。

计算机的实际 IP 地址数和计算机中安装的 NIC (网卡)数相同。即使是服务器,也需要知道 IP 地址是哪块 NIC 传来的,如果只有一块 NIC ,则直接使用 INADDR_ANY 。

除了 IP 地址,还有端口,如果将端口初始化为 0 ,就相当于把端口的选择权交由操作系统内核处理,操作系统内核会根据一定的算法来选择一个空闲的端口,这在服务器上不常用。

listen:接上电话线,一切准备就绪

bind 函数只是让为我们的套接字与地址相关,如同登记了电话号码,实际通信还需要接入电话线,让电话(服务器)处于真正可以接听状态。这是靠 listen 函数实现的。

初始化的套接字可以理解为 “主动” 套接字,如客户端创建的套接字主动调用 connect 向服务器创建连接,通过 listen 函数,将 “主动” 套接字变为 “被动” 套接字,告诉操作系统内核:“我这个套接字是用来等待用户请求的。” 操作系统内核也会为建立连接做足准备。

int listen (int socketfd, int backlog)
    
socketfd :套接字描述符
backlog :消息队列的长度,存放创建好连接但未accept的消息
accept:电话铃响了 ……

当客户端的连接请求到达之后,服务器做出应答,连接建立,此时操作系统内核需要通知应用程序客户端的请求已经到达,需要应用程序处理,就像电话铃响了,铃声就是在通知你接听。

连接建立之后,把 accept 当作操作系统与应用程序之间的桥梁!

int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)

listensocket :经过前面 bind 、listen 操作得到的套接字
函数返回两部分:
1、cliadd 是通过指针的方式获取的客户端地址,addrlen 代表地址大小,可以理解为看到来电提示,知道了对方的电话号码
2、返回一个全新的已连接描述字,代表与客户端的连接

为什么要返回一个新的套接字?以前那个不好吗?

这是因为,除了这个客户端要与服务器进行通信之外,还有其他许多客户端需要与服务器进行通信,你总不能让一个客户只能与一台服务器通信,其他就不能再与这台服务器进行通信了吧,这样的话需要多少台服务器啊?所以这样不行。因此是使用 accept 返回的全新的已连接套接字与客户端进行通信,当通信成功之后关闭的是这个已连接套接字而不是 listensocket ,listensocket 一直都在,它要跟成千上万个客户提供服务,因此 listensocket 要一直处于监听状态,一遍等待新的客户请求到达。

以上讲的都是服务器端,现在讲一讲客户端发起连接的过程。

创建套接字

和服务端一样。

connect:拨打电话

前面提及过,创建的套接字是 “主动” 套接字,所以通过 connect 函数来和服务器建立连接。

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
    
sockfd :连接套接字
servaddr :指向套接字地址结构的指针
addrlen :套接字地址结构的大小
套接字地址结构必须包含服务器的 IP 地址和 端口号

客户端无须调用 bind 函数绑定 IP 地址和端口号,这些事情是操作系统内核去做的。

TCP 套接字调用 connect 会激发 TCP 的三次握手,仅在连接建立成功或是出错时才返回。

出错情况(转载自极客时间):

  1. 对应服务器的 IP 地址写错,导致三次握手无法建立,客户端发出的 SYN 包没有任何相应,返回 TIMEOUT 错误。
  2. 客户端收到 RST (复位)回答,这时客户端会立即返回 CONNECTION REFUSED 错误。这种情况比较常见于客户端发送连接请求时的请求端口写错,因为 RST 是 TCP 在发生错误时发送的一种 TCP 分节。产生 RST 的三个条件是:目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器(如前所述);TCP 想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。
  3. 客户发出的 SYN 包在网络上引起了"destination unreachable",即目的不可达的错误。这种情况比较常见的原因是客户端和服务器端路由不通。

1.4 对套接字的读写操作

前面建立连接都是为数据的收发做准备。

Unix 不区分文件和套接字,Unix 下万物皆是文件。因此对文件的操作也可以用于 socket 。意味着可以将 socketfd 传递给原先为处理本地文件而设计的函数。

发送数据
ssize_t write (int socketfd, const void *buffer, size_t size)
ssize_t send (int socketfd, const void *buffer, size_t size, int flags)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)

write 函数时常见的文件写函数
send 函数用于向特定选项发送带外数据,带外数据是一种基于 TCP 协议的紧急数据,用于客户端-服务器的紧急处理
sendmsg 函数用于指定多重缓冲区传输数据

注意:socketfd 和普通的文件描述符并不一样。

对于普通文件描述符来说,一个文件描述符代表了打开的一个文件句柄,通过调用 write 函数,操作系统内核帮助我们不断向文件写入字节流,如果写入字节流大小与 size 不符,就会出错。

而对于 socketfd ,它代表的是双向连接,在 socketfd 上调用 write 写入的字节有可能比请求的数量少

发送缓冲区

当 TCP 连接创建成功,操作系统内核会为每一个连接创建配套的缓冲区。

因此,实际上,write 函数调用后并非立即传输数据, read 函数调用后也并非马上接收数据。

读取数据
ssize_t read (int socketfd, void *buffer, size_t size)

size 表示 read 要求操作系统内核从 socketfd 读取最多字节数
并将读取的字节数存入 buffer 中
返回实际读取的字节数
如果返回 0 ,表示 EOF ,在网络中表示发送了 FIN 包

一些文件操作符举例:

打开文件 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
open(const char *path , int flag);// path为文件地址, flag为文件开始模式,可能有多个,由|连接
例如 int fd = open("data.txt",O_CREAT|O_WRONLY|O_TRUNC);

O_CREAT     必要时创建文件
O_TRUNC     删除全部现有数据
O_APPEND    维持现有数据,保存到后面
O_RDONLY    只读打开
O_WRONLY    只写打开
O_RDWR      读写打开

关闭文件
#include <unistd.h>
int close(int fd);// fd为文件描述符

将数据写入文件
#include <unistd.h>
ssize_t write(int fd,const void * buf ,size_t nbytes)
size_t为无符号整形(unsigned int)的别名, ssize_tsigned int 类型
其中可以向数字写数据,例如 write(sock, (char*)&str_len, 4);	// 解释:str_len是int,向str_len中写4个Byte的int,然后转为char
通常是预先告知对方要发的字符串的大小为多少
    
读取文件中数据
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t nbytes);
// fd 文件描述符 ,buf 保存接收数据缓冲地址值 nbytes 接收数据最大字节数
也可以向数字读数据,例如 read(sock, (char*)&str_len, 4)	// 解释:读入四个字节存入str_Len中

1.5 杂谈

IP 是为收发网络数据而分配给计算机的值,端口号是为区分程序中创建的套接字而分配给套接字的序号,打个比方:IP 就是酒店的地址,而端口号就是酒店中房间的门牌号。

为保证数据正常接收,电脑都是先把数组转换为大端序再进行网络传输。网络字节序是大端序。

转为大端序的函数

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

htons 的 h 代表主机(host)字节序。transport short data from host to network
htons 的 n 代表网络(network)字节序。	transport short data from network to host

将字符产转为点分十进制的 IP 地址的函数

#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
// 成功时返回32位大端序整数,失败时返回InADDR_NONE

inet_aton 函数与 inet_addr 函数在功能上完全相同,也是将字符串形式的 IP 地址转换成整数型的 IP 地址。只不过该函数用了 in_addr 结构体,且使用频率更高。

#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
/*
成功时返回 1 ,失败时返回 0
string: 含有需要转换的IP地址信息的字符串地址值
addr: 将保存转换结果的 in_addr 结构体变量的地址值
*/

将网络字节整数 IP 地址转换成点分十进制的字符串形式:inet_ntoa

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr);
// 失败时返回-1
// 返回值是char指针要保存的话需要立刻复制字符串,下次调用后之前保存的字符串地址值失效

1.6 实例

实现迭代服务器端/客户端

What: 服务器端将客户端传输的字符串数据原封不动的传回客户端,就行回声一样

How:

1. 服务器端在同一时刻只与一个客户端相连,并提供回声服务
2. 服务器端依次向5个客户端提供服务并退出
3. 客户端接收用户输入的字符串并发送到服务器端
4. 服务器端将接收的字符串数据传回客户端,即回声
5. 服务器端与客户端之间的字符串回声一直执行到客户端输入Q为止

在原始版本迭代回声客户端代码存在一些问题:

write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE - 1);
message[str_len] = 0;
printf("Message from server: %s", message);

因为 TCP 不存在数据边界,多次调用的 write 函数传递的字符串可能一次性接收,也有可能字符串太长需要多次发送,但是客户端可能在尚未收到全部数据时就调用 read 函数。

改良版:

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

#define BUF_SIZE 1024

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

int main(int argc, char* argv[]) {
    int serv_sock = 0, clnt_sock = 0;
    char message[BUF_SIZE];
    int str_len = 0, i = 0;

    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t clnt_adr_sz;

    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("sock() error");
    // 套接字地址信息初始化
    memset(&serv_adr, 0, sizeof serv_sock);
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htons(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
	// 设定电话号码
    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof serv_adr) == -1)
        error_handling("bind() error");
	// 转为被动套接字
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    clnt_adr_sz = sizeof clnt_adr;

    for (int i = 0; i < 5; ++i) {
        // 创建新的连接套接字,接受连接请求
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if (clnt_sock == -1)
            error_handling("accept() error");
        else
            printf("Connect client %d \n", clnt_sock);
        // 进行读写操作
        while ((str_len = read(clnt_sock, message, BUF_SIZE)) != 0)
            write(clnt_sock, message, str_len);
        // 关闭连接套接字
        close(clnt_sock);
    }
    // 关闭监听套接字
    close(serv_sock);
    return 0;
}
client
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

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

int main(int argc, char* argv[]) {
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;

    if (argc != 3) {
        printf("Usage : %s <IP> <port> \n", argv[0]);
        exit(1);
    }
	// 创建主动套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) 
        error_handling("socket() error");
    // 初始化套接字地址信息
    memset(&serv_adr, 0, sizeof serv_adr);
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));
	// 向服务器发送连接请求
    if (connect(sock, (struct sockaddr*)&serv_adr, sizeof serv_adr) == -1)
        error_handling("connect() error");
    else 
        puts("Connected..........");

    while (1) {
        // 读写操作
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);

        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        
        write(sock, message, strlen(message));
        str_len = read(sock, message, BUF_SIZE - 1);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
    // 关闭套接字
    close(sock);
    return 0;
}
书上习题5.5

改写 tcp_server.c 和 tcp_client.c 客户端接受服务器端的字符串后便退出,需进行三次数据交换:

server:

// author : yumo
// date : 2021-10-26
// purpose : Learn how to define a protocol

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

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

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 message1[] = "Hello Client!";
    char message2[] = "I am server!";
    char message3[] = "Now we start to communicate!";
    char* str_arr[] = {message1, message2, message3};

    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    // Create socket
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);   
    if (serv_sock == -1) 
        error_handling("socket() error!");
    // Network address initialization
    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!");
    
    char read_buf[100];

    for (int i = 0; i < 3; ++i) {
        int str_len = strlen(str_arr[i]) + 1;
        write(clnt_sock, (char*)(&str_len), 4);
        write(clnt_sock, str_arr[i], str_len);

        read(clnt_sock, (char*)&str_len, 4);
        read(clnt_sock, read_buf, str_len);
        puts(read_buf);
    }

    close(clnt_sock);
    close(serv_sock);
    return 0;

}

client:

// author : yumo
// date : 2021-10-26
// purpose : familiar with tcp socket programing

#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) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int sock;
    struct sockaddr_in serv_addr;  

    char message1[] = "Hello Server!";
    char message2[] = "I am client!";
    char message3[] = "Now we start to communicate!";
    char* str_arr[] = {message1, message2, message3};
    char read_buf[100];

    int str_len = 0;


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

    sock = socket(PF_INET, SOCK_STREAM, 0); 
    if (sock == -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(sock, (struct sockaddr*)&serv_addr, sizeof serv_addr) == -1)    
        error_handling("connect() error!");
    else 
        puts("Connected............."); 
    
    for (int i = 0; i < 3; ++i) {
        read(sock, (char*)&str_len, 4);
        read(sock, read_buf, str_len);
        puts(read_buf);

        str_len = strlen(str_arr[i]) + 1;
        write(sock, (char*)&str_len, 4);
        write(sock, str_arr[i], str_len);
    }

    close(sock);
    return 0;
}

参考书籍及文章:

  • 《TCP/IP网络编程》
  • 极客时间专栏网络编程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值