4.1.网络编程之TCP通信

目录

网络知识介绍

Linux网络编程--初等网络函数介绍(TCP)

server.c(例程)

client.c(例程)


网络知识介绍

网络编程的一个最大的特点就是网络程序是由两个部分组成--客户端和服务器端。

客户端:在网络程序中,如果一个程序主动和外面的程序通信,那么我们把这个程序称为客户端程序。

服务端: 和客户端相对应的程序即为服务端程序。被动的等待外面的程序来和自己通讯的程序称为服务端程序。

实际生活中有些程序是互为服务和客户端。在这种情况项目, 一个程序既为客户端也是服务端。

Linux网络编程--初等网络函数介绍(TCP)

Linux系统通过提供套接字(socket)来进行网络编程的.网络程序通过调用socket和其它几个函数,返回一个通讯的文件描述符,我们可以将这个描述符看成普通的文件的描述符来操作,这就是linux的设备无关性的好处. 我们可以通过向描述符读写操作实现网络之间的数据交流.

服务器端:

(1)socket:相当于文件操作的open函数,用于创建一个类似于文件句柄的句柄fd

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

//domain:说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等). AF_UNIX只能够用于单一的Unix 系统进程间通信,而AF_INET是针对Internet的,因而可以允许在远程
//type:我们网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM等),SOCK_STREAM表明我们用的是TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流.SOCK_DGRAM 表明我们用的是UDP协议,这样只会提供定长的,不可靠,无连接的通信.
//protocol:由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了 socket为网络通讯做基本的准备.

(2)bind:一般打开文件也需要如路径等参数,进而将fd文件句柄与目标路径绑定,这里的bind也是类似的作用,但绑定的是主机的通讯簇类,端口,主机IP等信息。

int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

//sockfd:是由socket调用返回的文件描述符.
//addrlen:是sockaddr结构的长度.
//my_addr:是一个指向sockaddr的指针.

struct sockaddr
{
    unisgned short  as_family;
    char            sa_data[14];
};

//不过由于系统的兼容性,我们一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替.在中有sockaddr_in的定义
struct sockaddr_in
{
    unsigned short          sin_family;    
    unsigned short int      sin_port;
    struct in_addr          sin_addr;
    unsigned char           sin_zero[8];
};
/*
我们主要使用Internet所以
        sin_family一般为AF_INET,
        sin_addr设置为INADDR_ANY表示可以和任何的主机通信,
        sin_port是我们要监听的端口号,
        sin_zero[8]是用来填充的,
  bind将本地的端口同socket返回的文件描述符捆绑在一起.成功是返回0,失败的情况和socket一样
*/

(3)listen:用于监听服务器fd的信息

int listen(int sockfd,int backlog)

//sockfd:是bind后的文件描述符.
//backlog:设置请求排队的最大长度.当有多个客户端程序和服务端相连时, 使用这个表示可以接受的排队长度.listen函数将bind的文件描述符变为监听套接字.返回的情况和bind一样.

(4)accept:服务器端与客户端建立连接的函数,用于被动地等待客户端的连接请求

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

//sockfd:是listen后的文件描述符.
// addr,addrlen是用来给客户端的程序填写的,服务器端只要传递指针就可以了. bind,listen和accept是服务器端用的函数,accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接.accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了. 失败时返回-1

客户器端:

(5)connect:用于向服务端请求连接

int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)

//sockfd:socket返回的文件描述符.
//serv_addr:储存了服务器端的连接信息.其中sin_add是服务端的地址
//addrlen:serv_addr的长度
//connect函数是客户端用来同服务端连接的.成功时返回0,sockfd是同服务端通讯的文件描述符 失败时返回-1.

server.c(例程)

这里以韦东山的代码为例子,大致讲解一下下面这份代码各个部分的作用

#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


/* socket
 * bind
 * listen
 * accept
 * send/recv
 */

#define SERVER_PORT 8888
#define BACKLOG     10

int main(int argc, char **argv)
{
	int iSocketServer;
	int iSocketClient;
	struct sockaddr_in tSocketServerAddr;
	struct sockaddr_in tSocketClientAddr;
	int iRet;
	int iAddrLen;

	int iRecvLen;
	unsigned char ucRecvBuf[1000];

	int iClientNum = -1;

	signal(SIGCHLD,SIG_IGN);
	
	iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == iSocketServer)
	{
		printf("socket error!\n");
		return -1;
	}

	tSocketServerAddr.sin_family      = AF_INET;
	tSocketServerAddr.sin_port        = htons(SERVER_PORT);  /* host to net, short */
 	tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
	memset(tSocketServerAddr.sin_zero, 0, 8);
	
	iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
	if (-1 == iRet)
	{
		printf("bind error!\n");
		return -1;
	}

	iRet = listen(iSocketServer, BACKLOG);
	if (-1 == iRet)
	{
		printf("listen error!\n");
		return -1;
	}

	while (1)
	{
		iAddrLen = sizeof(struct sockaddr);
		iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
		if (-1 != iSocketClient)
		{
			iClientNum++;
			printf("Get connect from client %d : %s\n",  iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
			if (!fork())
			{
				while (1)
				{
					iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
					if (iRecvLen <= 0)
					{
						close(iSocketClient);
						return -1;
					}
					else
					{
						ucRecvBuf[iRecvLen] = '\0';
						printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
					}
				}				
			}
		}
	}
	
	close(iSocketServer);
	return 0;
}


第一部分:socket(服务端套接字创建)

iSocketServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iSocketServer)
{
    printf("socket error!\n");
    return -1;
}


/*

这段代码创建了一个用于 TCP 通信的服务器端套接字。其中,socket 函数用于创建一个新的套接字,它接受三个参数:
AF_INET:表示使用 IPv4 协议;
SOCK_STREAM:表示使用面向连接的流式套接字;(面向连接即TCP协议)
0:表示使用默认协议,通常为 TCP 协议。


如果 socket 函数返回值为 -1,说明创建套接字失败,此时会输出一条错误信息 "socket error!\n",并返回 -1。

如果成功创建套接字,会将其赋值给变量 iSocketServer,之后可以使用该套接字进行服务器端的网络通信。

*/

第二部分:bind(服务端套接字与服务端IP地址,端口号等信息绑定)

//前面的四句都是对结构体内容的填充
tSocketServerAddr.sin_family      = AF_INET;    //指定了地址族AF_INET(IPv4)其他参数:AF_INET6(IPv6)
tSocketServerAddr.sin_port        = htons(SERVER_PORT);  //指定了端口号,一般使用 htons 函数将端口号转换为网络字节序
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;    //INADDR_ANY 表示绑定到本地所有可用的 IP 地址。如ifconfig命令打印的eth33与eth36网卡信息中IP地址不同,INADDR_ANY将使得服务器端可以监听到全部网卡的IP地址。
memset(tSocketServerAddr.sin_zero, 0, 8);         //sin_zero 用于将结构体补齐到与 struct sockaddr 结构体相同的长度。
	
iRet = bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
    printf("bind error!\n");
    return -1;
}


/*
bind(iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));

第一个参数 sockfd 是服务器端套接字的文件描述符,它是由 socket 函数创建得到的。
第二个参数 addr 是一个 sockaddr 类型的指针,它指向要绑定的 IP 地址和端口号。
第三个参数 addrlen 是 addr 的大小,即要绑定的地址结构体的大小。
在该代码段中,第一个参数使用服务器端套接字的文件描述符 iSocketServer。第二个参数使用了结构体变量 tSocketServerAddr,该结构体变量包含了服务器端 IP 地址和端口号的信息
*/

第三部分:listen(开始监听服务器端套接字 iSocketServer,等待客户端的连接请求)

iRet = listen(iSocketServer, BACKLOG);
if (-1 == iRet)
{
    printf("listen error!\n");
    return -1;
}

/*

listen(iSocketServer, BACKLOG);

第一个参数 sockfd 是服务器端套接字的文件描述符,它是由 socket 函数创建得到的。
第二个参数 backlog 指定了连接队列的最大长度,也就是可以同时处理的等待连接请求的客户端的数量。

*/

第四部分:accept与send(之所以把这两个放在一起说,也是因为好像没办法细分了,)

/*
这段代码使用 accept 函数来接收客户端的连接请求,并启动子进程来处理客户端发来的消息。具体而言,while 循环会一直运行,等待客户端的连接请求。当一个客户端连接成功后,accept 函数会返回一个新的套接字 iSocketClient,该套接字用于和该客户端进行通信。
*/

    while (1)
	{
		iAddrLen = sizeof(struct sockaddr);
        //接受客户端连接请求,并返回一个客户端套接字iSocketClient 
		iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
        //如果接收到连接请求,则建立连接 
		if (-1 != iSocketClient)
		{
            //客户端计数变量加1,并打印连接的客户端的IP地址信息
			iClientNum++;
			printf("Get connect from client %d : %s\n",  iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
            //开启一个子进程,在if分支为子进程,else分支为主进程
			if (!fork())
			{    
                //在子进程中,程序会一直运行一个 while 循环,等待客户端发来的消息。如果接收到的消息长度小于等于 0,则表示客户端已经关闭连接,此时程序会关闭 iSocketClient 套接字,然后返回 -1,退出子进程。否则,程序会输出接收到的消息,然后继续等待客户端的下一条消息。
				while (1)
				{
					iRecvLen = recv(iSocketClient, ucRecvBuf, 999, 0);
					if (iRecvLen <= 0)
					{
						close(iSocketClient);
						return -1;
					}
					else
					{
						ucRecvBuf[iRecvLen] = '\0';
						printf("Get Msg From Client %d: %s\n", iClientNum, ucRecvBuf);
					}
				}				
			}
		}
	}
	//关闭服务器端套接字 iSocketServer
	close(iSocketServer);

附:僵尸进程解决(听课听得不是很认真,忘记咋形成了,后面有空再补充)

signal(SIGCHLD,SIG_IGN);

/*

这段代码是用来忽略 SIGCHLD 信号的,具体而言,它将 SIGCHLD 信号的处理方式设置为忽略。SIGCHLD 信号是在一个子进程终止或者停止时,由内核向其父进程发送的信号。如果父进程不对该信号进行处理,那么子进程就会变成僵尸进程,占用系统资源。因此,通常情况下,在父进程中需要对 SIGCHLD 信号进行处理,以避免产生僵尸进程。

有些情况下,我们希望让内核自动回收子进程,而不需要父进程对 SIGCHLD 信号进行处理。这时,我们可以使用 signal 函数将 SIGCHLD 信号的处理方式设置为 SIG_IGN(即忽略该信号),这样内核就会在子进程终止时自动回收它们,不会产生僵尸进程。

*/

client.c(例程)

#include           /* See NOTES */
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/* socket
 * connect
 * send/recv
 */

#define SERVER_PORT 8888

int main(int argc, char **argv)
{
	int iSocketClient;
	struct sockaddr_in tSocketServerAddr;
	
	int iRet;
	unsigned char ucSendBuf[1000];
	int iSendLen;

	if (argc != 2)
	{
		printf("Usage:\n");
		printf("%s \n", argv[0]);
		return -1;
	}

	iSocketClient = socket(AF_INET, SOCK_STREAM, 0);

	tSocketServerAddr.sin_family      = AF_INET;
	tSocketServerAddr.sin_port        = htons(SERVER_PORT);  /* host to net, short */
 	//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
 	if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
 	{
		printf("invalid server_ip\n");
		return -1;
	}
	memset(tSocketServerAddr.sin_zero, 0, 8);

	iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));	
	if (-1 == iRet)
	{
		printf("connect error!\n");
		return -1;
	}

	while (1)
	{
		if (fgets(ucSendBuf, 999, stdin))
		{
			iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
			if (iSendLen <= 0)
			{
				close(iSocketClient);
				return -1;
			}
		}
	}
	
	return 0;
}

第一部分:socket(与服务端一般无二,都是创建相同类型的数据结构而已)

iSocketClient = socket(AF_INET, SOCK_STREAM, 0);

/*

这段代码创建了一个用于 TCP 通信的服务器端套接字。其中,socket 函数用于创建一个新的套接字,它接受三个参数:
AF_INET:表示使用 IPv4 协议;
SOCK_STREAM:表示使用面向连接的流式套接字;(面向连接即TCP协议)
0:表示使用默认协议,通常为 TCP 协议。

*/

第二部分:conenect

tSocketServerAddr.sin_family      = AF_INET;    //使用 IPv4 地址族
tSocketServerAddr.sin_port        = htons(SERVER_PORT);  //将 sin_port 成员设置为服务器监听的端口号 SERVER_PORT,并使用 htons() 函数将主机字节序转换为网络字节序。
//代码使用 inet_aton() 函数将命令行参数 argv[1](表示服务器的 IP 地址)转换为网络字节序的二进制 IP 地址,并将其存储到 tSocketServerAddr.sin_addr 成员中。如果转换失败,函数会打印错误信息并返回 -1。
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
    printf("invalid server_ip\n");
    return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);

//它使用 connect() 函数,将创建的客户端套接字 iSocketClient 连接到指定的服务器地址 tSocketServerAddr,并传递一个指向该地址结构的指针。sizeof(struct sockaddr) 是地址结构的大小,这里需要将其传递给 connect() 函数,以指明要连接的地址结构的大小。
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));	
if (-1 == iRet)
{
    printf("connect error!\n");
    return -1;
}

第三部分:send/recv(每次从标准输入读取用户输入的字符串,然后使用 send() 函数将字符串发送到服务器)

    while (1)
	{
		if (fgets(ucSendBuf, 999, stdin))
		{
            //send() 函数将 ucSendBuf 中的字符串发送到连接的服务器,参数 iSendLen 返回发送的字节数
			iSendLen = send(iSocketClient, ucSendBuf, strlen(ucSendBuf), 0);
			if (iSendLen <= 0)
			{
                //如果 iSendLen 小于或等于 0,则意味着发送失败或连接已关闭。在这种情况下,应该关闭客户端套接字并退出程序。
				close(iSocketClient);
				return -1;
			}
		}
	}

综上,TCP通信编程的逐帧解析就到此为止,到时候UDP也会更新,但是可能晚点了,因为用的不是特别多

你可能感兴趣的:(tcp/ip,网络协议,网络)