ICS2课程笔记(四) | 计算机网络 | Lecture 4 网络编程

文章目录

lecture 4 ——网络编程

传输层的位置

在这里插入图片描述

在这里插入图片描述

传输层提供的服务

最低限度的传输服务
  • 终端-终端的数据交付扩展到进程-进程的数据交付

  • 报文检错

增强服务
  • 可靠数据传输

  • 流量控制

  • 拥塞控制

​因特网传输层通过UDP协议和TCP协议,向应用层提供两种不同的传输服务
  • UDP协议:仅提供最低限度的传输服务

  • TCP协议:提供基础服务和增强服务

创建一个网络应用

如何构建?

编写一个分布式程序,使其运行在不同的端系统上,并通过网络通信

选择一种应用程序体系结构
  • 传统及主流的是客户-服务器体系结构(C/S)

  • 有一台总是在线的主机,运行一个服务器程序(server),服务器主机具有永久的、众所周知的地址

  • 用户终端上运行一个客户程序(client),需要时主动与服务器程序通信,请求服务

  • 客户只与服务器通信,客户之间不通信

进程如何标识自己

每个进程需要一个标识,以便其它进程能够找到它
  • 问题:可以用进程所在主机的地址来标识进程吗?
    • 回答:不能!因为同一个主机上可能运行着许多进程
端口号(port number)
  • 端口号被用来区分同一个主机上的不同进程
进程标识包括两部分
  • 主机地址

  • 主机上与该进程关联的端口号

不同终端上的进程如何通信-Socket API

设想在应用程序和网络之间存在一扇“门”
  • 需要发送报文时:发送进程将报文推到门外

  • 门外的运输设施(因特网)将报文送到接收进程的门口

  • 需要接收报文时:接收进程打开门,即可收到报文

​在TCP/IP网络中,这扇“门”称为套接字(socket),是应用层和传输层的接口,也是应用程序和网络之间的API
  • Socket 套接字由ARPA资助UC Berkeley的一个研究组研发

  • 目的是将 TCP/IP 协议相关软件移植到 UNIX 类系统中,设计者开发了一个接口,形成了 Socket 套接字

  • Linux 系统采用了 Socket 套接字,成为事实标准

字节序与其转换

主要的字节存储机制:大端与小端
主机字节序(小端 Little-Endian)
  • 数据的低位字节存储到内存的低地址位 , 数据的高位字节存储到内存的高地址位(高存高,低存低)

  • 我们使用的 PC 机,数据的存储默认使用的是小端

网络字节序(大端Big-Endian)
  • 数据的低位字节存储到内存的高地址位 , 数据的高位字节存储到内存的低地址位(高存低,低存高)

  • 套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口

  • 比如用十六进制存储两个整数: 0 x 12345678 \mathrm{0x12345678} 0x12345678 0 x 11223344 \mathrm{0x11223344} 0x11223344

在这里插入图片描述

​字节序转换
BSD Socket 提供了转换接口
  • 主机字节序网络字节序的转换函数:htons、htonl

  • 网络字节序主机字节序的转换函数:ntohs、ntohl

IP 地址一般通过字符串描述,因此,通常使用专用转换API

将一个字符串类型的 IP 地址进行大小端转换

inet_ptoninet_ntop

//主机字节序的IP地址转换为网络字节序
//主机字节序的IP地址是字符串,网络字节序IP地址是整形
int inet_pton(int af,const char *src,void *dst);

参数:

  • af: 地址族(IP地址的家族包括ipv4和ipv6)协议

    • AF_INET \verb|AF_INET| AF_INET: ipv4格式的ip地址
    • AF_INET6 \verb|AF_INET6| AF_INET6: ipv6格式的ip地址
  • src:传入参数,对应要转换的点分十进制的ip地址:192.168.1.100

  • dst:传出参数,函数调用完成,转换得到的大端整形IP被写入到这块内存

  • 返回值:成功返回1,失败返回0或者-1

注意对比下面的函数

#include <arpa/inet.h>
//将大端的整形数,转换为小端的点分十进制的IP地址
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);

参数:

  • af: 地址族协议

    • AF_INET \verb|AF_INET| AF_INET: ipv4格式的ip地址
    • AF_INET6 \verb|AF_INET6| AF_INET6: ipv6格式的ip地址
  • src:传入参数,这个指针指向的内存中存储了大端的整形IP地址

  • dst:传出参数,存储转换得到的小端的点分十进制的IP地址

  • size:修饰dst参数的,标记dst指向的内存中最多可以存储多少个字节

▶️ 详情参考博客inet_pton()和inet_ntop()函数详解

套接字Socket

​通用地址 sockaddr \verb|sockaddr| sockaddr
  • sockaddr \verb|sockaddr| sockaddr是一个套接字API接口的通用地址类型

  • sa_family \verb|sa_family| sa_family表示地址族协议,一般采用“AF_xxx”形式

    • AF_INET:ipv4格式的ip地址

    • AF_INET6:ipv6格式的ip地址

    • AF_UNIX:unix格式的ip地址

  • sa_data \verb|sa_data| sa_data 表示地址:端口(2字节) + IP地址(4字节) + 填充(8字节)

//在写数据的时候不好用
struct sockaddr {
    sa_family_t sa_family; //地址族协议,ipv4
    char sa_data[14];//端口(2字节) + IP地址(4字节) + 填充(8字节)
}
IPV4地址 sockaddr_in \verb|sockaddr_in| sockaddr_in (相比 sockaddr \verb|sockaddr| sockaddr更方便使用)
  • sin_family:AF_INET(IPv4),编程时实际使用地址类型

  • sin_port:存放端口号(按照网络字节序存储)

  • sin_addr:存放32位IP地址(无符号整数)

  • sin_zero:为与sockaddr大小兼容而保留的空字节

struct sockaddr_in{//struct to hold an address
    sa_family_t sin_family;//always AF_INET
    in_port_t sin_port;//protocol port number:uint16_t
    struct in_addr_sin_addr;//IPaddress
    char sin_zero[8];//unused(set to zero)
};

struct in_addr{
    in_addr_t s_addr;//ipv4 address(uint32_t)
}
服务原语
原语发出的数据包含义
LISTEN(无)阻塞,直到某个进程试图与之连接
CONNECTCONNECTION REQ主动尝试建立一个连接
SENDDATA发送信息
RECEIVE(无)阻塞,直到有一个DATA数据包到达
DISCONNECTDISCONNECTION REQ请求释放连接
Berkeley套接字
原语含义
SOCKET创建一个新通信端点
BIND将套接字与一个本地地址关联
LISTEN声明愿意接受连接;给出队列大小
ACCEPT被动创建一个进来的连接
CONNECT主动创建一个连接
SEND通过连接发送一些数据
RECEIVE从连接上接收一些数据
CLOSE释放连接
创建套接字:socket()
  • 客户或服务器调用socket()创建本地套接字,返回套接字描述符

  • domain \verb|domain| domain指明网络层地址类型

  • type \verb|type| type指明传输层协议:

    • SOCK_STREAM \verb|SOCK_STREAM| SOCK_STREAM代表TCP字节流

    • SOCK_DGRAM \verb|SOCK_DGRAM| SOCK_DGRAM代表UDP数据报

#include <sys/socket.h>

int socket(
	int domain,/*AF_UNIX,AF_INET,AF_INET6.*/
    int type,/*SOCK_STREAM,SOCK_DGRAM*/
    int protocol); /*usuallyzero*/
/*Returnsfiledescriptoror-1onerror(setserrno)*/
/*函数的返回值是⼀个⽂件描述符,通过它以操作内核的某块内存,⽹络通信基于该⽂件描述符完成*/
/*如果包含头文件<arpa/inet.h>,就不用再包含<sys/socket.h> 了*/
绑定套接字地址:bind()
  • bind()用来文件描述符 sockfd \verb|sockfd| sockfd与本地地址 addr \verb|addr| addr绑定

  • 客户程序不需要调用bind(),操作系统将为其在1024~5000之间分配一个端口号

//将文件描述符和本地的IP与端口进行绑定
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

参数:

  • sockfd \verb|sockfd| sockfd:监听的文件描述符,通过socket ()调用得到的返回值

  • addr \verb|addr| addr:传入参数,要绑定的IP和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序

  • addrlen \verb|addrlen| addrlen:参数 addr \verb|addr| addr指向的内存大小,sizeof (struct sockaddr)

返回值:成功返回0,失败返回-1

监听套接字地址:listen()
  • listen()用来给监听的套接字设置监听

  • backlog \verb|backlog| backlog设定了监听队列长度

  • 客户程序不需要调用listen()

//给监听的套接字设置监听
int listen(int sockfd,int backlog);

参数:

  • sockfd \verb|sockfd| sockfd: 文件描述符,可以通过调用socket()得到,在监听之前必须要绑定bind()

  • backlog \verb|backlog| backlog: 同时能处理的最大连接要求,最大值为128

返回值:函数调用成功返回0,调用失败返回-1

接受套接字地址:accept()(是一个阻塞函数)
  • accept()用来等待并接受客户端的连接请求, 建立新的连接, 会得到一个新的文件描述符(通信的文件描述符)

  • 客户程序不需要调用accept()

//等待并接受客户端的连接请求,建立新的连接,会得到一个新的文件描述符(通信的)
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

参数:

  • sockfd \verb|sockfd| sockfd:监听的文件描述符

  • addr \verb|addr| addr:传出参数,里边存储了建立连接的客户端的地址信息

  • addrlen \verb|addrlen| addrlen:传入传出参数,用于存储 addr \verb|addr| addr指向的内存大小

返回值:函数调用成功,得到一个文件描述符,用于和建立连接的这个客户端通信,调用失败返回-1

接收数据套接字地址:read/recv()(阻塞函数)
ssize_t read(int sockfd,void *buf,size_t size);
ssize_t recv(int sockfd,void *buf,size_t size,int flags);

参数:

  • sockfd \verb|sockfd| sockfd:用于通信的文件描述符,accept()函数的返回值

  • buf \verb|buf| buf:指向一块有效内存,用于存储接收是数据

  • size \verb|size| size:参数 buf \verb|buf| buf指向的内存的容量

  • flags \verb|flags| flags:特殊的属性,一般不使用,指定为0

返回值:

  • 大于0:实际接收的字节数

  • 等于0:对方断开了连接

  • -1:接收数据失败了

发送数据套接字地址:write/send()(也是阻塞函数)
ssize_t write(int fd,const void *buf,size_t len);
ssize_t send(int fd,const void *buf,size_t len,int flags);

参数:

  • fd \verb|fd| fd: 通信的文件描述符,accept()函数的返回值

  • buf \verb|buf| buf: 传入参数,要发送的字符串

  • len \verb|len| len: 要发送的字符串的长度

  • flags \verb|flags| flags: 特殊的属性,一般不使用,指定为0

返回值:

  • 大于0: 实际发送的字节数,和参数 len \verb|len| len是相等的

  • -1: 发送数据失败了

连接套接字地址:connect()(三次握手导致connect()函数是阻塞函数)
  • connect()用来与服务端建立连接

  • 仅TCP客户程序调用connect()

//成功连接服务器之后,客户端会自动随机绑定一个端口
//服务器端调用accept()的函数,第二个参数存储的就是客户端的IP和端口信息
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

参数:

  • sockfd \verb|sockfd| sockfd: 通信的文件描述符|通过调用socket()函数就得到了

  • addr \verb|addr| addr: 存储了要连接的服务器端的地址信息:iP和端口,这个IP和端口也需要转换为大端然后再赋值

  • addrlen \verb|addrlen| addrlen: addr \verb|addr| addr指针指向的内存的大小sizeof (struct sockaddr)

返回值:连接成功返回0,连接失败返回-1

关闭套接字:close()
  • 客户和服务器调用close()关闭一个套接字

  • 当实参 fd \verb|fd| fd为TCP套接字描述符时,调用close()会引起本地进程向远程进程发送关闭连接的消息

  • 当实参 fd \verb|fd| fd为UDP套接字描述符时,调用close()会引起为此描述符分配的资源被内核释放

#include <unistd.h>
int close(int fd);
/*Returns0 if OK or-1onerror*/

▶️详情参考博客:传输层:(1)传输服务原语和Berkeley套接字

整理笔记时,考古到了01年的一个帖子,其中对于套接字的一些函数功能有着非常形象生动的概括:

socket()–得到文件描述符!

bind() --我们在哪个端口?

connect()–Hello!

listen() --有人给我打电话吗?

accept() --“Thank you for calling port 3490.”

send()recv() --Talk to me, baby!

sendto()recvfrom() --Talk to me, DGRAM-style

close()shutdown() --滚开!

getpeername()–你是谁?

gethostname() --我是谁?

DNS --你说“白宫”,我说 “198.137.240.100”

▶️ 详情参考博客:socket文件描述符

编程实例:回音服务

回音服务流程​
  • 客户程序将一行字符(数据),发送给服务器

  • 服务器接收数据,然后将收到的数据回送给客户

  • 客户接收回送的数据,在屏幕上显示出来

使用UDP套接字实现回音服务​
客户或服务器调用sendto()发送数据
  • 数据在 buff \verb|buff| buff中,长度为nbytes字节

  • 对方通信进程的地址在 to \verb|to| to

#include<sys/socket.h>
ssize_tsendto(
    int socket_fd,
	const void *buff,
	size_t nbytes,
	int flags, /* usually 0 */
	const struct sockaddr *to, 
	socklen_t *addrlen,
);
/*Return number of bytes written 
if OK or -1 on error*/
客户或服务器调用recvform()接受数据
  • 收到的数据放在 buff \verb|buff| buff

  • 对方通信进程的地址在 from \verb|from| from

#include<sys/socket.h>
ssize_t recvfrom(
    int socket_fd,
	void *buff,
	size_t nbytes,
	int flags, /* usually 0*/
	struct sockaddr *from, 
	socklen_t *addrlen,
);
/*Return number of bytes read if OK or -1 on error*/
基于UDP的套接字通信流程​

在这里插入图片描述

UDP回音服务代码​
服务端代码: main()
int main(int argc, char **argv){
	char mesg[MAXLINE];
	int sockfd, n, len;
    struct sockaddr_in cliaddr, servaddr;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);

	bind(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

	for ( ; ; ) {
	len = sizeof(cliaddr);
	n = recvfrom(sockfd, mesg, MAXLINE, 0, cliaddr, &len);
	sendto(sockfd, mesg, n, 0, cliaddr, len); 
	}
}
客户端代码
int main(int argc, char **argv){
	int sockfd, n;
	struct sockaddr_in servaddr;
	char sendline[MAXLINE], recvline[MAXLINE + 1]; 
    if (argc != 2){
	fprintf(stderr, %s\n, "usage: echoCli <IPaddress>");
	exit(EXIT_FAILURE);
	}
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET; 
	inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);
    
while (fgets(sendline, MAXLINE, stdin) != NULL) { 
	sendto(sockfd, sendline, strlen(sendline), 0, 
		(struct sockaddr *)servaddr, sizeof(servaddr));
	n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
	recvline[n] = 0;
	fputs(recvline, stdout);
	} 
	exit(0);
}
使用TCP套接字的通信流程

在这里插入图片描述

recvsend环节中可能发生阻塞问题

服务器使用多个套接字服务客户​

在这里插入图片描述

监听的文件描述符
  • 只需要一个

  • 不负责和客户端通信,负责检测客户端的连接请求,检测到之后调用accept就可以建立新的连接

通信的文件描述符
  • 负责和建立连接的客户端通信

如果有N个客户端和服务端建立了新的连接,通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符

📌 TCP回音过程代码

服务端代码
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
    
// 2. 将socket()返回值和本地的IP端⼝绑定到⼀起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // ⼤端端⼝
// INADDR_ANY代表本机的所有IP, 假设有三个⽹卡就有三个IP地址
// 这个宏可以代表任意⼀个IP地址
// 这个宏⼀般⽤于本地的绑定操作
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
    
// 3. 设置监听
ret = listen(lfd, 128);
if(ret == -1)
{
perror("listen");
exit(0);
}
    
// 4. 阻塞等待并接受客户端连接
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
if(cfd == -1)
{
perror("accept");
exit(0);
}
// 打印客户端的地址信息
char ip[24] = {0};
printf("客户端的IP地址: %s, 端⼝: %d\n"
,
inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(cliaddr.sin_port));
    
// 5. 和客户端通信
while(1)
{
// 接收数据
char buf[1024];
memset(buf, 0, sizeof(buf));
int len = read(cfd, buf, sizeof(buf));
if(len > 0)
{
printf("客户端say: %s\n", buf);
write(cfd, buf, len);
}
else if(len == 0)
{
printf("客户端断开了连接...\n");
break;
}
else
{
perror("read");
break;
}
}

// 6. 关闭连接
close(cfd);
close(lfd);
return 0;
}
客户端代码
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
    
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // ⼤端端⼝
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
int ret = connect(fd, (struct sockaddr*)&addr, 
sizeof(addr));
if(ret == -1)
{
perror("connect");
exit(0);
}
    
// 3. 和服务器端通信
int number = 0;
while(1)
{
// 发送数据
char buf[1024];
sprintf(buf, "你好, 服务器...%d\n", number++);
write(fd, buf, strlen(buf)+1);
// 接收数据
memset(buf, 0, sizeof(buf));
int len = read(fd, buf, sizeof(buf));
if(len > 0)
{
printf("服务器say: %s\n", buf);
}
else if(len == 0)
{
printf(“服务器断开了连接...\n”); break;
}
else
{
perror(“read”); break;
}
sleep(1); // 每隔1s发送⼀条数据
}
    
// 4. 关闭连接
close(fd);
return 0;
}
​三次握手与四次挥手

img

▶️ TCP部分的知识,可以参考大佬的博客:TCP 的那些事儿(上)TCP 的那些事儿(下),写的相当深入浅出

网络编程是构建分布式应用的基础,涉及进程间通信和数据传输。在传输层,TCP和UDP协议分别提供不同的服务:TCP保证数据的可靠传输,而UDP则提供较低延迟的简单传输服务。进程通过套接字(socket)API进行通信,套接字是应用层和传输层之间的接口,允许进程发送和接收数据。

套接字编程的核心在于几个关键的系统调用:socket()用于创建套接字,bind()将套接字与特定的地址和端口关联,listen()使套接字处于监听状态以接受连接,accept()接受连接请求,connect()由客户端用于建立连接,send()recv()用于数据传输,最后close()关闭套接字。此外,字节序转换是网络编程中的重要概念,因为网络通信中使用的是大端序,而多数现代系统使用小端序存储数据。

通过这些基本的套接字操作,开发者可以构建出功能丰富的网络应用,实现跨网络的数据交换和资源共享。网络编程不仅要求对套接字API有深刻理解,还需掌握字节序、IP地址处理等基础知识,以确保数据正确、高效地在网络中传输。

(Tips: 最近事情比较繁忙,笔者会尽可能抽出时间补上ICS2网络部分的前面三讲的笔记。本系列笔记均为笔者在课堂老师ppt笔记的基础上补充相关知识拼凑而成,鉴于笔者才疏学浅,文中若有知识性的谬误,还望各位读者朋友不吝赐教 !>_<!)

  • 31
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值