Linux网络编程

目录

网络通信概述

1.IP 和端口

2.网络传输中的 2 个对象:server 和 client

3.两种传输方式:TCP/UDP

① TCP 和 UDP 原理上的区别

② 为何存在 UDP 协议

③ TCP/UDP 网络通信大概交互图

网络编程主要函数介绍

socket 函数

bind 函数

listen 函数

accept 函数

connect 函数

send 函数

recv 函数

recvfrom 函数

sendto 函数

TCP 编程

UDP 编程


网络通信概述

1.IP 和端口

        所有的数据传输,都有三个要素 :源、目的、长度
        怎么表示源或者目的呢?如下图:

        所以,在网络传输中需要使用“IP 和端口”来表示源或目的。

2.网络传输中的 2 个对象:server 和 client

        我们经常访问网站,这涉及 2 个对象:网站服务器,浏览器。网站服务器平时安静地呆着,浏览器主动发起数据请求。网站服务器、浏览器可以抽象成 2 个软件的概念:server 程序、client 程序。 

3.两种传输方式:TCP/UDP

        在一般的网络书籍中,网络协议被分为 5 层:

 

        应用层:它是体系结构中的最高层,直接为用户的应用进程(例如电子邮件、文件传输和终端仿真)提供服务。在因特网中的应用层协议很多,如支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议,支持文件传送的 FTP 协议,DNS,POP3,SNMP,Telnet 等等。 
        运输层:负责向两个主机中进程之间的通信提供服务。
        运输层主要使用以下两种协议:
① 传输控制协议 TCP(Transmission Control Protocol):面向连接的,数据传输的单位是报文段,能够提供可靠的交付。
② 用户数据包协议 UDP(User Datagram Protocol):无连接的,数据传输的单位是用户数据报,不保证提供可靠的交付,只能提供“尽最大努力交付”。
        网络层:负责将被称为数据包(datagram)的网络层分组从一台主机移动到另一台主机。
        链路层:因特网的网络层通过源和目的地之间的一系列路由器路由数据报。
        物理层:在物理层上所传数据的单位是比特。物理层的任务就是透明地传送比特流。

        这些层对于初学者来说很难理解,我们只需要知道:我们需要使用“运输层”编写应用程序,我们的应用程序位于“应用层”。
        使用“运输层”时,可以选择 TCP 协议,也可以选择 UDP 协议。

① TCP 和 UDP 原理上的区别

        TCP 向它的应用程序提供了面向连接的服务。这种服务有 2 个特点:可靠传输、流量控制(即发送方/接收方速率匹配)。它包括了应用层报文划分为短报文,并提供拥塞控制机制。
        UDP 协议向它的应用程序提供无连接服务。它没有可靠性,没有流量控制,也没有拥塞控制。

② 为何存在 UDP 协议

既然 TCP 提供了可靠数据传输服务,而 UDP 不能提供,那么 TCP 是否总是首选呢?

        答案是否定的,因为有许多应用更适合用 UDP,举个例子:视频通话时,使用 UDP,偶尔的丢包、偶尔的花屏时可以忍受的;如果使用 TCP,每个数据包都要确保可靠传输,当它出错时就重传,这会导致后续的数据包被阻滞,视频效果反而不好。
        使用 UDP 时,有如下特点:
① 关于何时发送什么数据控制的更为精细
        采用 UDP 时只要应用进程将数据传递给 UDP,UDP 就会立即将其传递给网络层。而 TCP 有重传机制,而不管可靠交付需要多长时间。但是实时应用通常不希望过分的延迟报文段的传送,且能容忍一部分数据丢失。
② 无需建立连接,不会引入建立连接时的延迟。
③ 无连接状态,能支持更多的活跃客户。
④ 分组首部开销较小。

③ TCP/UDP 网络通信大概交互图

        下面我们分别画出运用 TCP 协议和运用 UDP 协议的客户端和服务器大概交互图。

面向连接的 TCP 流模式:

UDP 用户数据包模式: 


网络编程主要函数介绍

socket 函数

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

此函数用于创建一个套接字。
\bullet domain 是网络程序所在的主机采用的通讯协族(AF_UNIX 和 AF_INET 等)。
        ◼ AF_UNIX 只能够用于单一的 Unix 系统进程间通信,而 AF_INET 是针对 Internet 的,因而可以允许远程通信使用。
\bullet type 是网络程序所采用的通讯协议(SOCK_STREAM,SOCK_DGRAM 等)。
        ◼ SOCK_STREAM 表明用的是 TCP 协议,这样会提供按顺序的,可靠,双向,面向连接的比特流。
        ◼ SOCK_DGRAM 表明用的是 UDP 协议,这样只会提不可靠,无连接的通信。
\bullet 关于 protocol,由于指定了 type,所以这个地方一般只要用 0 来代替就可以了。
        此函数执行成功时返回文件描述符,失败时返回-1,看 errno 可知道出错的详细情况。


bind 函数

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

此函数用于将地址绑定到一个套接字(用socket创建的套接字)
\bullet sockfd 是由 socket 函数调用返回的文件描述符。
\bullet my_addr 是一个指向 sockaddr 的指针。
\bullet addrlen 是 sockaddr 结构的长度。

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 sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8];
}

如果使用 Internet 所以
\bullet sin_family 一般为 AF_INET
\bullet sin_port 是要监听的端口号。
\bullet sin_addr 设置为 INADDR_ANY 表示可以和任何的主机通信。
\bullet sin_zero[8] 是用来填充的。
\bullet bind 将本地的端口同 socket 返回的文件描述符捆绑在一起.成功是返回 0,失败的情况和 socket 一样。


listen 函数

int listen(int sockfd, int backlog);

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


accept 函数

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

服务器使用此函数获得连接请求,并且建立连接。
\bullet sockfd 是 listen 后的文件描述符。
\bullet addraddrlen 是用来给客户端的程序填写的,服务器端只要传递指针就可以了, bind,listen 和 accept 是服务器端用的函数。
\bullet accept 调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接。accept 成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了,失败时返回-1 。


connect 函数

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

可以用 connect 建立一个连接,在 connect 中所指定的地址是想与之通信的服务器的地址。
\bullet sockfd 是 socket 函数返回的文件描述符。
\bullet serv_addr 储存了服务器端的连接信息,其中 sin_add 是服务端的地址。
\bullet addrlen 是 serv_addr 的长度。
\bullet connect 函数是客户端用来同服务端连接的,成功时返回 0,sockfd 是同服务端通讯的文件描述符,失败时返回-1。


send 函数

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

\bullet sockfd 指定发送端套接字描述符;
\bullet buf 指明一个存放应用程序要发送数据的缓冲区;
\bullet len 指明实际要发送的数据的字节数;
\bullet flags 一般置 0。
\bullet 客户或者服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数据


recv 函数

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

\bullet sockfd 指定接收端套接字描述符;
\bullet buf 指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;
\bullet len 指明 buf 的长度;
\bullet flags 一般置 0。
\bullet 客户或者服务器应用程序都用 recv 函数从 TCP 连接的另一端接收数据。


recvfrom 函数

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

\bullet recvfrom 通常用于无连接套接字,因为此函数可以获得发送者的地址。
\bullet src_addr 是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地址及端口号。
\bullet addrlen 常设置为 sizeof(struct sockaddr)。


sendto 函数

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

\bullet sendto 和 send 相似,区别在于 sendto 允许在无连接的套接字上指定一个目标地址
\bullet dest_addr 表示目地机的 IP 地址和端口号信息。
\bullet addrlen 常常被赋值为 sizeof (struct sockaddr)。
\bullet sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。


TCP 编程

写代码实现一个服务端能接收多个客户端的连接请求

server.c

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>


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

/*定义要监听的端口*/
#define SERVER_PORT 8888
/*listen函数的第二个参数,表示最大能同时监测多少路连接*/
#define BACKLOG     10

int main(int argc, char **argv)
{
    /*存放服务端和客户端的文件描述符*/
	int iSocketServer;
	int iSocketClient;

    /*使用bind函数时需要传入sockaddr_in结构体*/
	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;
    /*htons表示将主机字节序转化为网络字节序,大小为2字节*/
	tSocketServerAddr.sin_port        = htons(SERVER_PORT);  /* host to net, short */
    /*我们的主机可能有多个端口,设置成INADDR_ANY表示能监听所有的端口*/
 	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);
        /*accept等待连接,客户端的信息会保存在tSocketClientAddr里面*/
		iSocketClient = accept(iSocketServer, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
		if (-1 != iSocketClient)
		{
            /* 如果accept函数的返回值不等于-1表示连接成功
             * iClientNum统计客户端的数量,相当于id
             */
			iClientNum++;
            
            /*打印客户端的IP信息,用inet_ntoa将网络字节序转化为ASCII码*/
			printf("Get connect from client %d : %s\n",  iClientNum, inet_ntoa(tSocketClientAddr.sin_addr));
			if (fork() == 0)
			{
				/*fork返回值是0表示是子进程,这里是子进程的代码*/
				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;
}

client.c

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

/* 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;

    /*如果用法不对的话打印用法,需要用户传入想要连接的IP地址*/
	if (argc != 2)
	{
		printf("Usage:\n");
		printf("%s <server_ip>\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;
    /*用inet_aton函数将字符串转化为网络字节序,传入结构体对于的参数里去,设置想要连接的IP地址*/
 	if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
 	{
		printf("invalid server_ip\n");
		return -1;
	}
	memset(tSocketServerAddr.sin_zero, 0, 8);

    /*调用connect函数连接*/
	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;
}

现象:

        左边是服务端,右边我运行了两个客户端,没有输入IP会打印用法,连接后就可以给服务端发送消息了,服务端会显示客户端的ID号。


UDP 编程

        UDP连接的服务端不需要调用listen和accept函数,socket函数创建套接字后用bind绑定IP地址等等,然后调用recvfrom函数接收数据;客户端和TCP类似,可以调用sendto或者send函数来发送数据,我们直接写代码:

server.c

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>


/* socket
 * bind
 * sendto/recvfrom
 */

#define SERVER_PORT 8888

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;
	
    /*第二个参数改成SOCK_DGRAM表示用UDP连接*/
	iSocketServer = socket(AF_INET, SOCK_DGRAM, 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;
	}


	while (1)
	{
		iAddrLen = sizeof(struct sockaddr);
		iRecvLen = recvfrom(iSocketServer, ucRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
		if (iRecvLen > 0)
		{
			ucRecvBuf[iRecvLen] = '\0';
			printf("Get Msg From %s : %s\n", inet_ntoa(tSocketClientAddr.sin_addr), ucRecvBuf);
		}
	}
	
	close(iSocketServer);
	return 0;
}

client.c
(和tcp的客户端几乎一样,只需要改变一下socket函数的第二个参数即可)

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

/* 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 <server_ip>\n", argv[0]);
		return -1;
	}

    /**/
	iSocketClient = socket(AF_INET, SOCK_DGRAM, 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;
}

        效果我就不在演示了,和TCP的效果差不多。

        需要注意的是:使用UDP编程时,客户端的connect函数可以不用,但是发送数据需要使用sendto函数,sendto函数会指定要发送的服务器的IP地址。

上一篇:编码算法笔记(base64,url编码等)


下一篇:PyAutoGUI自动完成重复性任务