网络通信 - d14

一、基本概念

1.协议模型(OSI模型)

应用层 - 应用程序
表示层 - 数据的组织
会话层 - 数据的交换
传输层 - 数据的封包
网络层 - 网络的拓扑结构
数据链路层 - 数字信号的电子化
物理层 - 网络设备

2.TCP/IP协议族

  • 1)TCP(Transmission Control Protocol),传输控制协议
面向连接的协议。
逻辑上模拟为电话业务。
  • 2)UDP(User Datagram Protocol),用户数据报文协议
面向无连接的协议。
逻辑上模拟为邮政业务。
  • 3)IP(Internet Protocol),互联网协议
工作在TCP和UDP协议的底层。
实现在互联网上传递信息的基本机制。

ISO           TCP/IP
应用层
表示层        应用层      telnet/ftp/http
会话层
------------------------
传输层        传输层      TCP/UDP
网络层        互联网层    IP
------------------------
数据链路层    网络接口层  硬件/驱动
物理层

3.消息流和协议栈

有协议模型的不同层次构成了一个自下而上的协议栈, 所发送数据的数据沿着协议栈自上而下层层打包, 最终变成可以在物理介质上传输的电子信号。 接收数据时再将物理介质上的电子信号,沿着协议栈自下向上, 层层解包,最终变成应用可以处理的数据内容。

4.IP地址

  • 1)IP地址是Internet中唯一标识一台计算机的地址。
A.IPv4:32位,IPv6:128位
B.点分十进制字符串表示:0x01020304 -> 1.2.3.4
  • 2)IP地址分级
A级:0XXXXXXX | XXXXXXXX XXXXXXXX XXXXXXXX
         网络地址       本机地址
B级:10XXXXXX XXXXXXXX | XXXXXXXX XXXXXXXX
         网络地址           本机地址
C级:110XXXXX XXXXXXXX XXXXXXXX | XXXXXXXX
网络地址                          本机地址
172.40.0.10
D级:1110XXXX XXXXXXXX XXXXXXXX XXXXXXXX
         多播地址
  • 3)子网掩码
IP地址 & 子网掩码 = 网络地址
IP地址 & ~子网掩码 = 本机地址
IP地址:192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182.0
本机地址:0.0.0.48

二、套接字(Socket)

1.接口

QQ      \
LeapFTP  > socket -> TCP/UDP -> IP -> 网络驱动 -> 网络硬件
IE      /

2.异构

Java@UNIX@大型机<->socket<->C@Windows@PC机

3.模式

  • 1)点对点(Peer-to-Peer,P2P),一对一通信。
  • 2)客户机/服务器(Client/Server,C/S),一对多通信。
  • 3)浏览器/服务器(Browser/Server,B/S),一个服务器为多个浏览器提供服务。

4.绑定

将逻辑的套接字对象与物理的通信载体关联起来。

5.函数

1)创建套接字

#include <sys/socket.h>
int socket (int domain, int type, int protocol);
  • domain - 域
AF_UNIX/AF_LOCAL/AF_FILE,本地通信
AF_INET,基于IPv4的网络通信, 对于BSD是AF_INET,对于POSIX是PF_INET. 两者本质一样。
AF_INET6,基于IPv6的网络通信
AF_PACKET,基于IP的底层协议进行网络通信
  • type - 类型
SOCKET_STREAM,流式套接字,基于TCP协议通信
SOCKET_DGRAM,数据报套接字,基于UDP协议通信
  • protocal - 特殊协议,目前置0即可
成功返回套接字描述符,失败返回-1。
套接字描述符类似于文件描述符,UNIX把网络当成文件看待。
发送数据即写文件,接收数据即读文件,一切皆文件。
  • 2)通信地址

A.基本地址结构

struct sockaddr {
  sa_family_t sa_family; // 地址族
  char sa_data[14];      // 地址值
};

B.网络地址结构

#include <netinet/in.h>

struct sockaddr_in {
  sa_family_t sin_family; /* 地址族,AF_INET/AF_INET6 */
  in_port_t    sin_port;  /* unsigned short,网络字节序的端口号
                              逻辑上表示一个参与通信的进程,
                              0-1024,公知端口,WWW-80
                              FTP-21,TELNET-23,...
                              自己编写的应用程序使用1024以上的端口号 */
  struct in_addr sin_addr; /* 网络字节序的IP地址 */
};
小低低:小端字节序,低位低地址
int i = 0x12345678;
小端机器:低地址-------->高地址
                78      56     34      12  -> 0x78563412
大端机器:低地址-------->高地址
                12      34     56      78

主机字节序->网络字节序->发送
接收->网络字节序->主机字节序
网络字节序就是大端字节序
欲从小端机器发送0x12345678:L 78 56 34 12 H
现将其转换为网络字节序:L 12 34 56 78 H
发送到大端机器上:L 12 34 56 78 H
从网络字节序转到大端字节序:L 12 34 56 78 H -> 0x12345678
struct in_addr {
  in_addr_t s_addr;
};
typedef uint32_t in_addr_t;
typedef unsigned int uint32_t;
  • 3)将套接字和通信地址绑定
int bind (int sockfd, const string sockaddr* addr, socklen_t addrlen);

// 成功返回0,失败返回-1。
  • 4)将套接字和对方通信地址连接
int connect (int sockfd, const string sockaddr* addr, socklen_t addrlen);

// 成功返回0,失败返回-1。
  • 5)用读写文件的方式通信:read/write
  • 6)关闭套接字:close
  • 7)字节序转换
#include <arpa/inet.h>

// 主机字节序->网络字节序
uint32_t htonl (uint32_t hostlong);
uint16_t htons (uint16_t hostshort);

// 网络字节序->主机字节序
uint32_t ntohl (uint32_t netlong);
uint16_t ntohs (uint16_t netshort);
  • 8)IP地址转换
//点分十进制字符串->网络字节序整数形式的IP地址
in_addr_t inet_addr (const char* cp);

//网络字节序整数形式的IP地址->点分十进制字符串
char* inet_ntoa (struct in_addr in);

6.编程

服务器:创建套接字 -> 准备地址结构并绑定 -> 接收数据 -> 关闭套接字
客户机:创建套接字 -> 准备地址结构并连接 -> 发送数据 -> 关闭套接字

三、基于TCP协议的客户机/服务器模型

1.TCP协议的基本特征

  • 1)面向连接:虚拟电路,建立可靠的连接,稳定地传输数据,人为断开连接。
  • 2)传输可靠:发送数据、等待确认、丢包重传。
ABCDEF
A->              -+
B->               |
C->               | 时间窗口
D->               |
E-> <-A OK -+
F-> <-B OK
       <-C OK
       <-D OK
       <-E OK
       <-F OK

每个发送都有应答,若在一定的时间窗口内没有收到应答,
重新再发。
  • 3)保证顺序:有序发送,有序接收,丢弃重复。
  • 4)流量控制:接收端实时通知发送端接收窗口大小,防止溢出。
  • 5)传输速度慢。

2.编程模型

服务器                             客户机
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
创建套接字 socket           创建套接字 socket
准备地址   sockaddr_in      准备地址   sockaddr_in
绑定地址   bind
监听套接字 listen
接受连接   accept           建立连接   connect
接收数据   read/recv        发送数据   write/send
发送数据   write/send       接收数据   read/recv
关闭套接字 close            关闭套接字 close

3.常用函数

#include <sys/socket.h>
int listen (
    int sockfd,   // 套接字描述符
    int backlog   // 未决连接请求队列的最大长度,1024
);
//成功返回0,失败返回-1。

int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
/* 从sockfd参数所标识套接字的未决连接请求队列中, 提取第一个连接请求,同时创建一个新的套接字,
用于在该连接中通信,返回该套接字描述符。
同时通过后两个参数向调用者输出客户机的地址信息。
失败返回-1。
*/

ssize_t recv (int sockfd, void* buf, size_t len, int flags);
// 返回实际接收到的字节数。
// 如果在该函数执行过程中,对方关闭了连接,返回0。
// 失败返回-1。

ssize_t send (int sockfd, void const* buf, size_t len, int flags);
//返回kk实际发送的字节数。失败返回-1。

example

  • UDP
/*
 * file : netcli.c
 */

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

int main (int argc, char* argv[]) {
	if (argc < 3) {
		printf ("用法:%s <IP地址> <端口号>\n",
			argv[0]);
		return -1;
	}
	printf ("客户机:创建套接字...\n");
	int sockfd = socket (AF_INET, SOCK_DGRAM,
		0);
	if (sockfd == -1) {
		perror ("socket");
		return -1;
	}
	printf ("客户机:准备地址并连接...\n");
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons (atoi (argv[2]));
	addr.sin_addr.s_addr = inet_addr (argv[1]);
	if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
		perror ("connect");
		return -1;
	}
	printf ("客户机:发送数据...\n");
	for (;;) {
		printf ("> ");
		char buf[1024];
		gets (buf);
		if (! strcmp (buf, "!"))
			break;
		if (write (sockfd, buf, strlen (buf) + 1) == -1) {
			perror ("write");
			return -1;
		}
		if (! strcmp (buf, "!!"))
			break;
	}
	printf ("客户机:关闭套接字...\n");
	close (sockfd);
	return 0;
}
/*
 * filename : netsvr.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main (int argc, char* argv[]) {
	if (argc < 2) {
		printf ("用法:%s <端口号>\n", argv[0]);
		return -1;
	}
	printf ("服务器:创建套接字...\n");
	int sockfd = socket (AF_INET, SOCK_DGRAM,0);
	if (sockfd == -1) {
		perror ("socket");
		return -1;
	}
	printf ("服务器:准备地址并绑定...\n");
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons (atoi (argv[1]));
	addr.sin_addr.s_addr = INADDR_ANY;
	if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) {
		perror ("bind");
		return -1;
	}
	printf ("服务器:接收数据...\n");
	for (;;) {
		char buf[1024];
		ssize_t rb = read (sockfd, buf, sizeof (buf));
		if (rb == -1) {
			perror ("read");
			return -1;
		}
		if (! strcmp (buf, "!!"))
			break;
		printf ("< %s\n", buf);
	}
	printf ("服务器:关闭套接字...\n");
	close (sockfd);
	return 0;
}
  • TCP
/*
 * filename :  tcpsvr.c
 */

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

int main (int argc, char* argv[]) {
	if (argc < 2) {
		printf ("用法:%s <端口号>\n", argv[0]);
		return -1;
	}
	printf ("服务器:创建套接字...\n");
	int sockfd = socket (AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1) {
		perror ("socket");
		return -1;
	}
	printf ("服务器:准备地址并绑定...\n");
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons (atoi (argv[1]));
	addr.sin_addr.s_addr = INADDR_ANY;
	if (bind (sockfd, (struct sockaddr*)&addr,
		           sizeof (addr)) == -1) {
		perror ("bind");
		return -1;
	}
	printf ("服务器:监听套接字...\n");
	if (listen (sockfd, 1024) == -1) {
		perror ("listen");
		return -1;
	}
	printf ("服务器:等待连接请求...\n");
	struct sockaddr_in addrcli = {};
	socklen_t addrlen = sizeof (addrcli);
	int connfd = accept (sockfd,
		(struct sockaddr*)&addrcli, &addrlen);
	printf ("服务器:接受来自%s:%u客户机的"
		"连接请求。\n",
		inet_ntoa (addrcli.sin_addr),
		ntohs (addrcli.sin_port));
	printf ("服务器:收发数据...\n");
	for (;;) {
		char buf[1024];
		ssize_t rb = recv (connfd, buf,
			sizeof (buf), 0);
		if (rb == -1) {
			perror ("recv");
			return -1;
		}
		if (rb == 0) {
			printf ("服务器:客户机已关闭连接。\n");
			break;
		}
		if (send (connfd, buf, rb, 0) == -1) {
			perror ("send");
			return -1;
		}
	}
	printf ("服务器:关闭套接字...\n");
	close (connfd);
	close (sockfd);
	return 0;
}
  • 信号量 sem
/*
 * filename : csem.c
 */

#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>

int pmenu (void) {
	printf ("------------------\n");
	printf ("    迷你图书馆\n");
	printf ("------------------\n");
	printf ("[1] 借《三国演义》\n");
	printf ("[2] 还《三国演义》\n");
	printf ("[3] 借《水浒传》\n");
	printf ("[4] 还《水浒传》\n");
	printf ("[5] 借《红楼梦》\n");
	printf ("[6] 还《红楼梦》\n");
	printf ("[7] 借《西游记》\n");
	printf ("[8] 还《西游记》\n");
	printf ("[0] 退出\n");
	printf ("------------------\n");
	printf ("请选择:");
	int sel = -1;
	scanf ("%d", &sel);
	return sel;
}

int pleft (int semid, unsigned short semnum){
	int val = semctl (semid, semnum, GETVAL);
	if (val == -1) {
		perror ("semctl");
		return -1;
	}
	printf ("还剩%d册。\n", val);
	return 0;
}

int borrow (int semid, unsigned short semnum){
	struct sembuf sop = {semnum, -1,
		/*0*/IPC_NOWAIT};
	if (semop (semid, &sop, 1) == -1) {
		if (errno != EAGAIN) {
			perror ("semop");
			return -1;
		}
		printf ("暂时无书,下回再试。\n");
		return 0;
	}
	printf ("借阅成功。\n");
	return pleft (semid, semnum);
}
int revert (int semid, unsigned short semnum){
	struct sembuf sop = {semnum, 1, 0};
	if (semop (semid, &sop, 1) == -1) {
		perror ("semop");
		return -1;
	}
	printf ("归还成功。\n");
	return pleft (semid, semnum);
}
int main (void) {
	printf ("创建信号量...\n");
	key_t key = ftok (".", 100);
	if (key == -1) {
		perror ("ftok");
		return -1;
	}
	int semid = semget (key, 4, 0644 |
		IPC_CREAT | IPC_EXCL);
	if (semid == -1) {
		perror ("semget");
		return -1;
	}
	printf ("初始化信号量...\n");
	unsigned short semarr[] = {5, 5, 5, 5};
	if (semctl (semid, 0, SETALL,
		semarr) == -1) {
		perror ("semctl");
		return -1;
	}
	int quit = 0;
	while (! quit) {
		int sel = pmenu ();
		switch (sel) {
			case 0:
				quit = 1;
				break;
			case 1:
			case 3:
			case 5:
			case 7:
				if (borrow (semid, sel / 2) == -1)
					return -1;
				break;
			case 2:
			case 4:
			case 6:
			case 8:
				if (revert (semid, (sel-1)/2) == -1)
					return -1;
				break;
			default:
				printf ("无效选择!\n");
				scanf ("%*[^\n]");
				scanf ("%*c");
				break;
		}
	}
	printf ("销毁信号量...\n");
	if (semctl (semid, 0, IPC_RMID) == -1) {
		perror ("semctl");
		return -1;
	}
	printf ("再见!\n");
	return 0;
}
/*
 * gsem.c 
 */

#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>

int pmenu (void) {
	printf ("------------------\n");
	printf ("    迷你图书馆\n");
	printf ("------------------\n");
	printf ("[1] 借《三国演义》\n");
	printf ("[2] 还《三国演义》\n");
	printf ("[3] 借《水浒传》\n");
	printf ("[4] 还《水浒传》\n");
	printf ("[5] 借《红楼梦》\n");
	printf ("[6] 还《红楼梦》\n");
	printf ("[7] 借《西游记》\n");
	printf ("[8] 还《西游记》\n");
	printf ("[0] 退出\n");
	printf ("------------------\n");
	printf ("请选择:");
	int sel = -1;
	scanf ("%d", &sel);
	return sel;
}
int pleft (int semid, unsigned short semnum){
	int val = semctl (semid, semnum, GETVAL);
	if (val == -1) {
		perror ("semctl");
		return -1;
	}
	printf ("还剩%d册。\n", val);
	return 0;
}
int borrow (int semid, unsigned short semnum){
	struct sembuf sop = {semnum, -1,
		/*0*/IPC_NOWAIT};
	if (semop (semid, &sop, 1) == -1) {
		if (errno != EAGAIN) {
			perror ("semop");
			return -1;
		}
		printf ("暂时无书,下回再试。\n");
		return 0;
	}
	printf ("借阅成功。\n");
	return pleft (semid, semnum);
}
int revert (int semid, unsigned short semnum){
	struct sembuf sop = {semnum, 1, 0};
	if (semop (semid, &sop, 1) == -1) {
		perror ("semop");
		return -1;
	}
	printf ("归还成功。\n");
	return pleft (semid, semnum);
}
int main (void) {
	printf ("创建信号量...\n");
	key_t key = ftok (".", 100);
	if (key == -1) {
		perror ("ftok");
		return -1;
	}
	int semid = semget (key, 0, 0);
	if (semid == -1) {
		perror ("semget");
		return -1;
	}
	int quit = 0;
	while (! quit) {
		int sel = pmenu ();
		switch (sel) {
			case 0:
				quit = 1;
				break;
			case 1:
			case 3:
			case 5:
			case 7:
				if (borrow (semid, sel / 2) == -1)
					return -1;
				break;
			case 2:
			case 4:
			case 6:
			case 8:
				if (revert (semid, (sel-1)/2) == -1)
					return -1;
				break;
			default:
				printf ("无效选择!\n");
				scanf ("%*[^\n]");
				scanf ("%*c");
				break;
		}
	}
	printf ("再见!\n");
	return 0;
}