Skip to content

LV015-TCP协议编程

本文主要是网络编程——TCP协议下的socket编程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、TCP编程流程

1. TCP编程步骤

首先我们需要知道使用TCP编程时的基本步骤:

1.1 客户端

(1)调用socket()函数打开套接字,得到套接字描述符;

(2)调用bind()函数将套接字与IP地址、端口号进行绑定(这一步也可以没有);

(3)调用connect()函数向服务器发送连接请求并建立连接;

(4)调用read/recv、write/send与客户端进行通信;

(5)调用close()关闭套接字。

1.2 服务器端

(1)调用socket()函数打开套接字,得到套接字描述符;

(2)调用bind()函数将套接字与IP地址、端口号进行绑定;

(3)调用listen()函数让服务器进程进入监听状态;

(4)调用accept()函数获取客户端的连接请求并建立连接,若无客户端请求连接,这里会进行阻塞等待;

(5)调用read/recv、write/send与客户端进行通信;

(6)调用close()关闭套接字。

2. TCP编程流程图

TCP编程过程的基本流程图如下:

image-20220627064517694

二、TCP连接建立

前边我们已经学习了socket相关API函数,下边我们就使用socket创建一个TCP协议的连接。

1. server服务器端

首先我们来编写服务器端实现连接的程序。

1.1 需要注意的地方

(1)前边已经提过了,服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。所以我们绑定的IP一定要是运行服务器端程序的计算机本地网卡的IP地址,注意这个IP可能不止一个,如我在虚拟机中安装的Ubuntu

image-20220627101449145

这就意味着我们使用bind绑定的时候,这两个IP都可以使用。

(2)当我们绑定的IP不是上边的本地IP的时候可能会报以下错误:

shell
Cannot assign requested address

(3)我们要是想监听这两个IP地址的话,就需要两个套接字,其实前边已经提过了,我们可以通过一个套接字来管理两个IP,这个就是通过绑定INADDR_ANY,这就带代表了我们可以监听所有本地的IP,这样不管客户端是连接了哪个网卡,我们都可以在服务器端监听到连接状态,这样客户端便都可以正常连接过来了。不过这个直接就是一个二进制类型的IP,我们使用的时候可以直接进行字节序转换,然后可以直接填充到相应的结构体中。或者就是直截了当,使用0.0.0.0,这其实与INADDR_ANY效果是一样的,个人觉得吧,这样似乎还更通用一点,可以随时改服务器绑定的IP

1.2 服务器端实例

c
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <sys/types.h>  /* socket           bind listen accept */
#include <sys/socket.h> /* socket inet_addr bind listen accept */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);
	if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
	{
		perror("accept");
		exit(-1);
	}
	/* 5.打印成功连接的客户端的信息(此处只能打印一个) */
	char ipv4_addr[16];
	if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
	{
		perror ("inet_ntop");
		exit(-1);
	}

	printf ("Clinet(%s:%d) is connected successfully!\n", ipv4_addr, ntohs(cin.sin_port));

	/* 6.数据读写 */
	while (1)
	{
		sleep(1);
	}
	/* 7.关闭文件描述符 */
	close(newfd);
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2. client客户端

接下来就是我们来编写客户端实现连接的程序啦。

2.1 需要注意的地方

(1)我们客户端connect的时候,填写的IP必须是运行服务器进程的计算机的本地IP,例如,我在虚拟机的Ubuntu中有两个网卡,这两个网卡的IP为:

shell
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.101  netmask 255.255.255.0  broadcast 192.168.10.255
ens37: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.239.128  netmask 255.255.255.0  broadcast 192.168.239.255

这就意味着我们连接的应该是192.168.10.101或者192.168.239.128,若为其他,则可能会报:

shell
connect: No route to host

(2)还有一种情况,就是我们服务器进程的bind()函数绑定了一个Ip,但是我们的客户端绑定了另一个IP,例如,我们的服务器进程绑定的是192.168.10.101,但是客户端通过connect连接的却是192.168.239.128,这时候虽然不会报上边(1)的错误吗,但是会报如下错误:

shell
connect: Connection refused

(3)若想客户端连接哪个IP都可以,这个时候我们可以让服务器的bind()绑定IP的时候绑定到0.0.0.0,或者使用INADDR_ANY

2.2 客户端实例

c
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket connect */
#include <sys/socket.h>/* socket inet_addr connect */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/
	   
#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	
	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	while (1)
	{
		sleep(1);
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
}

3. Makefile文件

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

makefile
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
all : $(TARGET_LIST)

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

4. 测试结果

我们首先运行make命令来编译生成文件:

shell
make

开始测试之前吗,我们需要知道,我所使用的Ubuntu网卡的所有IP,我们可以使用以下命令查看:

shell
ifconfig

然后我们便会看到以下信息:

shell
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.101  netmask 255.255.255.0  broadcast 192.168.10.255
        inet6 fe80::8c02:d542:197d:a89b  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:45:70:eb  txqueuelen 1000  (以太网)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 91  bytes 9743 (9.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

ens37: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.239.128  netmask 255.255.255.0  broadcast 192.168.239.255
        inet6 fe80::ec67:9bd4:65eb:469f  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:45:70:f5  txqueuelen 1000  (以太网)
        RX packets 7725  bytes 2146206 (2.1 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1198  bytes 101872 (101.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (本地环回)
        RX packets 395  bytes 30869 (30.8 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 395  bytes 30869 (30.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

4.1 测试1

首先我们进行服务器端绑定IP的情况的测试,我们在终端中输入以下命令:

shell
./server 192.168.10.101 5001
./server 192.168.239.128 5001
./server 127.0.0.1 5001
./server 0.0.0.0 5001

./server 192.168.10.102 5001

然后会得到下图的情况:

image-20220627112039254

4.2 测试2

这个测试是使用客户端连接服务器。

4.2.1 服务器IP固定为网卡IP
shell
./server 192.168.10.101 5001

然后,会有如下提示:

shell
Server starting....OK!

之后该服务器开始阻塞等待客户端的连接。然后我们重新开一个终端,输入以下命令:

shell
./client 192.168.10.102 5001
./client 192.168.239.128 5001

./client 192.168.10.101 5001

然后我们会看到如下图的情况:

image-20220627112840663

前两次的时候分别报了上边曾经说过的错误,只有最后一次是连接成功的。

4.2.2 服务器IP固定为INADDR_ANY

为了方便测试,上边的源程序中使用的是0.0.0.0,但是这跟INADDR_ANY是一样的。

shell
./server 0.0.0.0 5001

然后,会有如下提示:

shell
Server starting....OK!

之后该服务器开始阻塞等待客户端的连接。然后我们重新开一个终端,输入以下命令:

shell
./client 192.168.10.102 5001
./client 192.168.239.128 5001

./client 192.168.10.101 5001

然后我们会看到如下图的情况:

image-20220627121857381

由于我没有设置服务器端循环等待客户端的连接,所以服务器端只能连接一个客户端,所以上图中服务器端运行了两次,看输出结果我们会发现,当服务器IP设置为0.0.0.0的时候,客户端连接的只要是运行服务器端的计算机的本地网卡IP,都可以正常连接到服务器。

5. 端口号问题

从上边的两个测试中,有木有发现一个问题,就是每一次客户端连接到服务器的时候,明明都是同一个程序,但是端口号却每次都不一样,而且这个端口号还跟我们客户端连接的时候的不一样。

后来查阅了一些资料,了解到服务器用listen()监听时要指定一个监听端口号,例如上边的5001,而客户端用connect()连接时要指定服务器地址以及服务器的提供的连接端口号(也就是5001),客户端本机向外连接的端口号由本机随机产生,这样的好处就是可以避免端口冲突。

还记得上边的TCP编程流程图吧,里边有一个虚框框住的bind(),这时候一想,是不是发现前边我们的服务器好像用bind()函数绑定了IP和端口号,但是客户端的程序却没有这个操作,根据bind()函数的功能,我们很容易想到,如果说客户端也进行Ip和端口号的绑定,那这个服务器读取到的端口号是否就是固定的呢?答案是肯定的,不过需要注意的是,绑定的端口不能与其他的端口冲突。下边可以进行一个测试。

我们修改客户端程序如下,这里需要注意要保证客户端绑定的端口号要跟连接到服务器的端口号不同:

c
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket connect */
#include <sys/socket.h>/* socket inet_addr connect */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/
	   
#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}
	
	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 新增:绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero (&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	
	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	while (1)
	{
		sleep(1);
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

这个时候我们按照相同的方式分别运行两个程序:

shell
./server 0.0.0.0 5001

./client 192.168.10.101 5001 5002
image-20220627193650781

会发现如图中,服务器端读取到的端口号与我们绑定的端口号一样了。

6. 有一个问题

不知道前边有没有发现一个问题,就是有的时候我们服务器和客户端退出后,立刻重新运行服务器端,可能会发现如下报错:

shell
bind: Address already in use

这是因为,我们给给某一进程分配端口,但是进程结束没有释放这一端口,我们可以在创建完socket套接字后添加以下内容:

c
/* 允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

函数介绍以及各个参数说明会在后边学习网络属性设置的时候会再说。

三、数据传输

当我们建立好连接后,接下来就是数据的传输了。

1. 单向传输

1.1 server服务器端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>     /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept */
#include <sys/socket.h> /* socket inet_addr bind listen accept */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */
#include <string.h>     /* strlen */

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);
	if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
	{
		perror("accept");
		exit(-1);
	}
	/* 5.打印成功连接的客户端的信息(此处只能打印一个) */
	char ipv4_addr[16];
	if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
	{
		perror ("inet_ntop");
		exit(-1);
	}

	printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);

	/* 6.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	while(1)
	{
		bzero(buf, BUFSIZ);
		do{
			ret = read(newfd, buf, BUFSIZ - 1);
		}while(ret < 0 && EINTR == errno);
		if(ret < 0)
		{
			perror("read");
			exit(-1);
		}
		if(!ret) break; /* 对方已经关闭 */
		printf("Receive data: %s\n", buf);

		if(!strncasecmp (buf, "quit", strlen("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 7.关闭文件描述符 */
	close(newfd);
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

1.2 client客户端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket connect */
#include <sys/socket.h>/* socket inet_addr connect */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	while (1)
	{
		bzero (buf, BUFSIZ);
		if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
		{
			continue;
		}
		do{
			ret = write(socket_fd, buf, strlen(buf));
		}while (ret < 0 && EINTR == errno);

		if (!strncasecmp (buf, "quit", strlen ("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

1.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
all : $(TARGET_LIST)

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

1.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

对于服务器端,我们执行以下命令启动服务器进程:

shell
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

shell
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

image-20220628202029504

可以发现我们发送的数据,在客户端都被打印了出来,说明客户端与服务器端可以进行正常通信。

2. 双向传输

TCP中,有两组数据的接收和发送,这里客户端向服务器端发送数据的时候我们使用read()/write(),而服务器端向客户端回复的时候我们使用recv()/send()函数,这样也熟悉一下这两组数据接收发送函数。

2.1 server服务器端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: server.c
 * Author   : fanhua
 * Description: server服务器端——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>      /* perror */
#include <stdlib.h>     /* exit atoi */
#include <errno.h>     /* errno号 */
#include <sys/types.h>  /* socket           bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h>    /* bzero */
#include <arpa/inet.h>  /* htons  inet_addr inet_pton */
#include <netinet/in.h> /* ntohs  inet_addr inet_ntop*/
#include <unistd.h>     /* close */
#include <string.h>     /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	if (argc != 3)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	if (port < 5000)
	{
		usage(argv[0]);
		exit(-1);
	}
	/* 1.创建套接字,得到socket描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.将套接字与指定端口号和IP进行绑定 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin));  /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;    /* 协议族, IPv4 */
	sin.sin_port = htons(port);  /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 2.2绑定 */
	if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("bind");
		exit(-1);
	}
	/*3.调用listen()把主动套接字变成被动套接字 */
	if (listen(socket_fd, 5) < 0)
	{
		perror("listen");
		exit(-1);
	}
	printf ("Server starting....OK!\n");
	/*4.阻塞等待客户端连接请求 */
	int newfd = -1;
	struct sockaddr_in cin;          /* 用于存储成功连接的客户端的IP信息 */
	socklen_t addrlen = sizeof(cin);
	if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
	{
		perror("accept");
		exit(-1);
	}
	/* 5.打印成功连接的客户端的信息(此处只能打印一个) */
	char ipv4_addr[16];
	if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
	{
		perror ("inet_ntop");
		exit(-1);
	}

	printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);

	/* 6.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while(1)
	{
		/* 6.接收来自客户端的数据 */
		bzero(buf, BUFSIZ);
		bzero(replay, BUFSIZ);
		do{
			ret = read(newfd, buf, BUFSIZ - 1);
		}while(ret < 0 && EINTR == errno);
		if(ret < 0)
		{
			perror("read");
			exit(-1);
		}
		if(!ret) break; /* 对方已经关闭 */
		printf("Receive data: %s\n", buf);
		/* 6.2对客户端做出应答 */
		strcat(replay, buf);
		ret = send(newfd, replay, strlen(replay), 0);
		if(ret < 0)
		{
			perror("send");
			exit(-1);
		}
		/* 6.3判断是否需要退出 */
		if(!strncasecmp(buf, "quit", strlen("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 7.关闭文件描述符 */
	close(newfd);
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

2.2 client客户端

c
/** =====================================================
 * Copyright © hk. 2022-2022. All rights reserved.
 * File name: cilent.c
 * Author   : fanhua
 * Description: client客户端程序——TCP
 * ======================================================
 */
/* 头文件 */
#include <stdio.h>     /* perror fgets */
#include <stdlib.h>    /* exit atoi*/
#include <errno.h>     /* errno号 */
#include <unistd.h>    /* write close */

#include <sys/types.h> /* socket           connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h>   /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
	/* 参数判断及端口号处理 */
	int port = -1;
	int portClient = -1;
	if (argc != 4)/* 参数数量不对时打印提示信息 */
	{
		usage(argv[0]);
		exit(-1);
	}
	port = atoi(argv[2]);/* 字符串转数字 */
	portClient = atoi(argv[3]);
	if (port < 5000 || portClient < 5000 || (port == portClient))
	{
		usage(argv[0]);
		exit(-1);
	}

	/* 1.打开套接字,得到套接字描述符 */
	int socket_fd = -1; /* 接收服务器端socket描述符 */
	if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		perror ("socket");
		exit(-1);
	}
	/* 允许绑定地址快速重用 */
	int b_reuse = 1;
	setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
	/* 2.连接服务器 */
	/* 2.1填充struct sockaddr_in结构体变量 */
	struct sockaddr_in sin;
	bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
	sin.sin_family = AF_INET;   /* 协议族 */
	sin.sin_port = htons(port); /* 网络字节序的端口号 */
	/* 客户端的 argv[1] 需要与系统的IP一致 */
	if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	/* 绑定固定的IP 和 端口号 */
	struct sockaddr_in sinClient;
	bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
	sinClient.sin_family = AF_INET;   /* 协议族 */
	sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
	if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
	{
		perror ("inet_pton");
		exit(-1);
	}
	if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
	{
		perror("bind");
		exit(-1);
	}

	/* 2.2连接 */
	if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
	{
		perror("connect");
		exit(-1);
	}
	printf("Client staring...OK!\n");
	/* 3.数据读写 */
	int ret = -1;
	char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
	char replay[BUFSIZ];
	while (1)
	{
		/* 6.1发送数据 */
		bzero (buf, BUFSIZ);
		bzero (replay, BUFSIZ);
		printf(">");
		if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
		{
			continue;
		}
		do{
			ret = write(socket_fd, buf, strlen(buf));
		}while (ret < 0 && EINTR == errno);
		/* 6.2接收服务器的回馈数据 */
		ret = recv(socket_fd, replay, BUFSIZ, 0);
		if(ret < 0)
		{
			perror("recv");
			exit(-1);
		}
		printf("server replay:%s\n", replay);
		/* 6.3判断是否需要退出 */
		if (!strncasecmp(buf, "quit", strlen ("quit")))  	//用户输入了quit字符
		{
			printf ("Client is exiting!\n");
			break;
		}
	}
	/* 4.关闭文件描述符 */
	close(socket_fd);

	return 0;
}

/**
 * @Function: usage
 * @Description: 用户提示信息打印函数
 * @param str : 当前应用程序命令字符串,一般是./app
 * @return  : none
 */
void usage(char *str)
{
	printf ("\n%s serv_ip serv_port", str);
	printf ("\n\t serv_ip: server ip address");
	printf ("\n\t serv_port: server port(>5000)\n\n");
	printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}

2.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

makefile
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author   : fanhua
# Description: Makefile文件
## ======================================================
# 
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} 
all : $(TARGET_LIST)

%.o : %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
	@rm -vf $(TARGET_LIST) 
	
clean_o :
	@rm -vf *.o
	
clean_out :
	@rm -vf *.out

2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

对于服务器端,我们执行以下命令启动服务器进程:

shell
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

shell
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

image-20220628205333470

可以发现我们发送的数据,在服务器端都被打印了出来,客户端也打印出了服务器端发送的数据。说明客户端与服务器端可以进行正常双向通信。