2024年6月4号,网络编程技术课程期末复习
题型:
选择题知识点:
基本概念;网络编程常识;基本函数使用;三元组和五元组;字节转换;熟知端口;多线程有关的理论;接收数据的处理流式套接字;windows编程三个题,有有关图形界面的;多线程同步问题;阻塞非阻塞;组播;非阻塞模式下错误处理相关内容;异步选择模型;netstat命令。
简答题知识点:
绪论一道8分;绪论幻灯片有一道4分,跟乱码有关系;所有编程模式总结,单线程、多线程等,考的与并发性有关;第四个是程序填空写代码
最后一道大题程序设计非阻塞模式服务端,16分
组播和广播一样,都是对范围内的一定设备进行报文传输的一种方式。组播是主机间一对多的通讯模式, 组播是一种允许一个或多个组播源发送同一报文到多个接收者的技术。和对多个主机进行单播,数据源要同时发出多份报文不同,对多个主机进行组播时,数据源实际仅需要发出一份报文。并且,组播可以跨网段传输。
IPv4组播使用D类地址空间,所有地址的最高位固定为1110,所以实际组播可以使用的地址范围为224.0.1.0到239.255.255.255,其中224.0.0.x保留为路由协议等使用。
三元组:ip地址,端口,三层协议,用于标识一个网络上的一个主机的进程。
五元组:源ip地址,目标ip地址,源端口,目标端口,四层协议,用于标识网络上两个主机的一个会话。
netstat命令用于显示Linux系统的整个网络状态,它的语法如下:
netstat [-acCeFghilMnNoprstuvVwx][-A<网络类型>][--ip]
它的可选参数如下:
提示
注意参数只有一个-a的话,netstat不会显示Listen状态的连接的,必须使用-l
个人比较常用的一个netstat格式是这样的:netstat -tunlp | grep ':80'
,显示tcp和udp下的所有连接,直接显示ip,并且显示连接的程序的信息,另外还对':80'这个字符串进行查找,用于查找是否有程序占用了80端口。
以下为IANA规定的,一些为应用层协议而保留的常用端口。这些仅为推荐使用,不代表实际使用中一定要用这些端口。反过来说,这些0到1024的常用端口也极其不推荐给别的程序使用。
老师应该不考1024以上的常用端口吧(?)
提示
QUIC协议,由Google的一名员工设计,以udp协议为基础,试图改进HTTP协议中速度不够快并且一旦出错就会阻塞发送过程的问题。该协议在2018年10月在HTTP3中正式替代TCP,并且2021年5月推出标准化版本。
在所有的平台上,多字节对象都会被存储为连续的字节。连续就会有顺序,就有了大端序和小端序这两种顺序差别。
因为小端序更符合计算机的读取顺序,所以采用小端序的计算机一般比大端序更快,所以如今的计算机大多采用小端序。但是网络传输或者文件存储,一般会为人类阅读的,并且不影响处理效率的,则采用小端序。
关于大端序(网络序)和小端序(主机序)转换,需要四个函数:
示例:
cppint port = atoi(argv[2]); // 获取运行参数port
addr.sin_port = htons(port); // 转换网络序
这个题实在太宽泛了,随便写点吧。
计算机的发展史,就是对已经有的各类技术进行封装和抽象,并在此基础上发展新的技术的过程。网络通信也是如此,一次封装和抽象就是一个技术栈,栈与栈的不停叠加,最终就形成了今天发达的互联网。
按照五层模型来说的话:
网络编程,可能涉及到的方向有网络层(网络安全,时延控制,可靠传输等方向,或者是对已有的传输层两大协议进行改进)、传输层(制订应用层协议方向,比如上面提到过的QUIC)和应用层(应用层协议的二次开发方向,比如基于HTTP的RPC服务)
Client/Server模型(简称C/S模型)是最常见的通信模型,由一个Server/Server集群为任意的Client提供服务。
Server的特性包括被动等待客户端的连接,连续、长期、稳定运行,一般需要较多资源和稳定的操作系统支持,可同时服务多个客户端等。Client的特性包括主动发起连接请求,可以访问多种服务端,对硬件和操作系统要求不高等。
客户端和服务端本质上都是具有通信功能的进程,一个计算机极有可能同时运行多种客户端和服务端,同时一个进程有可能既是客户端又是服务端。
不同的操作系统通信要考虑的问题包括:差错控制与处理,字节序,字长,字节定界问题和字符编码,时间表示等等。
int pthread_create(pthread_t *restrict threadID, const pthread_attr_t *restrict attr, void * ( *start_routine)(void*), void * restrict arg)
,创建线程,start_routine即为创建的新的线程的执行函数。int pthread_join(pthread_t threadID, void **status)
,等待线程,阻塞当前线程,直至threadID所对应的线程执行结束并释放资源。int pthread_detach(pthread_t threadID)
,分离线程,threadID对应的线程会在执行结束后自动释放资源,并且不会阻塞调用方线程。void pthread_exit(void *retval)
,结束调用方所在线程。注意主线程调用return会导致进程的结束,所有线程不管执行是否结束都会一并结束;调用pthread_exit则不会强行结束其他线程。进程是操作系统用于管理运行过程中的程序实例的基本单位,由代码,数据和进程控制块PCB组成。线程是缩小版本的进程,它是CPU调度的基本单位,共享进程的资源,有自己独立的栈和寄存器,每个进程至少包括一个主线程,主线程执行完毕后进程也将结束。
线程的运行是异步的,各自运行各自的,线程间通信一般有这么几个方法:
以c为例,线程间通信的方式可以有:
c语言下的互斥锁:
pthread_mutex_init(pthread_mutex_t*, const pthread_mutexattr_t*)
,创建锁。pthread_mutex_lock(pthread_mutex_t*)
,加锁。pthread_mutex_unlock(pthread_mutex_t*)
,解锁。pthread_mutex_destroy(pthread_mutex_t*)
,销毁锁。c语言下的信号量:
int sem_init(sem_t *sem, int pshared, int value)
,创建信号量并且指定信号量初始值。如果该信号量仅在当前进程下使用,那么pshared参数指定为0。int sem_wait(sem_t *sem)
,信号量-1,如果不能减1那么会阻塞线程直至在别的线程中信号量+1为止。int sem_post(sem_t *sem)
,信号量+1。int sem_destroy(sem_t *sem)
,销毁信号量。select模型会将程序所创建的所有套接字存入三个集合之后,交给操作系统进行管理,如果这些套接字中有产生了新的连接/收到了新的消息,那么select会返回发生了变化的套接字的个数,由程序自己自行检查哪些套接字发生变化。
select模型使用(注意,fd_set是专为文件描述符准备的集合类型):
FD_ZERO(fd_set* set)
函数,将三个集合(可读性集合,可写性集合,错误集合)清零。FD_SET(int fd, fd_set* set)
函数,将套接字按需加入集合。提示
select函数的五个参数分别是:
int maxfd
,要select的套接字数量fd_set *readfds
,可读性集合fd_set *writefds
,可写性集合fd_set *exceptfds
,错误/意外集合const struct timeval *timeout
,要等待的时间,select会阻塞线程timeout这么长时间。如果这里传入NULL,则select会一直阻塞线程,直到所有集合上有套接字符合条件;如果传入0,则会立即返回。注意,Windows和Linux的select参数有一些不同。Windows端的maxfd参数是无意义的,默认为0,而Linux的maxfd参数是有实际意义的;Windows的timeout仅需设置一次,而Linux的timeout必须每次都设置(这个“每次都设置”,是因为select会修改这个timeout)。除此之外,Windows的这个fd_set大小默认为64,如果要修改大小,要在include <winsock2.h>
前定义;而Linux默认1024,可以任意修改大小。
FD_ISSET(int fd, fd_set* set)
函数判断是否还在集合中。如果还在,则进行处理,该读的读,该写的写。提示
select的返回值可以是0(时间截止,仍没有一个套接字符合要求),-1(发生错误)或者是其他的值,表示状态变化符合条件的套接字数量。
select的缺点在于,每次调用select,这些套接字集合都有从用户态向内核态,再从内核态向用户态的拷贝过程;另外,select会对所有套接字进行遍历,select结束后程序也大概率会对所有套接字进行遍历,这在套接字过多时耗时容易过长。
epoll模型在select模型上更进一步,不但返回发生了变化的套接字个数,还会将哪些套接字发生了什么事件一并返回,程序就不需要再对所有套接字进行检查,仅需要对发生的时间和套接字进行处理即可。
epoll没学Winsock相关。
epoll模型使用:
create_epoll(int maxfds)
创建一个epoll的句柄,其中maxfds为epoll所支持的最大套接字数量(新版本后取消该参数)。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
来将套接字添加进epoll。op类似select的那三个集合条件,分别为EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。epoll_event
的数组,数组长度等于create_epoll(int maxfds)
中传入的数字。然后循环调用epoll_wait(int epfd, strcuct epoll_event* events, int maxevents, int timeout)
,调用结束后,遍历epoll_event
,通过数组中的事件来处理对应套接字,其形式大概类似这样:cstruct epoll_event events[MAX_EVENTS]; // 事件数组
int numEvents = epoll_wait(epollfd, events, MAX_EVENTS, timeout);
for (int i = 0; i < numEvents; i++) {
int sockfd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
// sockfd 就绪,可读事件发生
// 处理可读事件的逻辑...
}
if (events[i].events & EPOLLOUT) {
// sockfd 就绪,可写事件发生
// 处理可写事件的逻辑...
}
// 处理其他事件类型...
}
epoll模型的两种工作模式:
epoll_wait
都会返回该套接字对应的事件,直到程序处理该事件。epoll_wait
只会返回一次该套接字对应的事件。即使程序不处理它,下次epoll它仍然满足条件,该套接字对应事件也不会出现在epoll_event数组中了。epoll事件有以下类型:
EPOLLIN
:表示可读事件,当文件描述符上有数据可读时触发。EPOLLOUT
:表示可写事件,当文件描述符可写入数据时触发。EPOLLPRI
:表示紧急事件,当文件描述符上有紧急数据可读时触发。EPOLLERR
:表示错误事件,当文件描述符发生错误时触发。EPOLLHUP
:表示挂起事件,当文件描述符挂起(例如对端关闭连接)时触发。EPOLLET
:表示边缘触发模式,使用该模式可以实现高效的事件驱动。EPOLLONESHOT
:表示单次触发模式,当事件发生后,文件描述符将被从 epoll 集合中移除,需要重新添加才能继续监听该文件描述符。int socket(int domain, int type, int protocol)
,创建套接字,domain用于指定套接字的通信域,一般情况下直接用AF_INET
即可;type用于指定套接字类型,常用的为SOCK_DGRAM(UDP),DOCK_STREAM(TCP);protocol一般为0。inet_addr([]char)
,用于转换字符串ip到in_addr_t类型。int bind(int sockfd, struct sockaddr *local_addr, int addrlen)
,绑定ip。int connect(int sockfd, struct sockaddr *peer_addr, int addrlen)
,连接远程套接字。int listen(int sockfd, int backlog)
,监听,tcp专属;backlog参数指定请求队列长度,如果它满,则客户端想要发起连接时会报错ECONNREFUSED
。int accept(int sockfd, void *addr, int *addrlen)
,接收连接,并且返回用于和客户端通信的套接字。int send(int sockfd, constant void *msg, int len, int flags)
,发送数据,flags一般为0。该函数仅将数据从用户态复制到内核态,何时发送由操作系统决定。int write(int sockfd, constant void *msg, int len)
,当sockfd为套接字时,功能和send一致。int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int *tolen)
,指定地址发送,udp专属版send。它和send都是如果发送成功,就返回实际发送的字节数,反之则返回-1。int recv(int sockfd, const void *buf, int len, int flags)
,接收数据,并将数据写入buf数组。int read(int sockfd, const void *buf, int len)
,读取数据,比较类似write和send的关系。int recvfrom(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *from, int *fromlen)
,类似sendto和send的关系,但是udp在一些情况下也能使用recv和send。int setsockopt(int sockfd, int level, int name, char *value, int *optlen)
,int getsockopt(int sockfd, int level, int name, char *value, int *optlen)
,一个设置套接字选项,一个获取套接字设置,主要用来设置广播/多播。int ioctl(int fd, unsigned long request,…)
,设置文件描述符选项,可以设置套接字的阻塞/非阻塞模式。int close(int fd)
,关闭套接字。int shutdown(int fd, int how)
,关闭数据传输,它应先于close使用,但是不调用它直接close一般也不会出问题。int getpeername(int sockfd, struct sockaddr *addr, int *addrlen)
,获取套接字对端信息。int gethostname(char *hostname, size_t size)
,获取主机名。c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, max_fd, activity, client_socket[MAX_CLIENTS], i, valread, sd;
int max_sd;
fd_set readfds;
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr;
// 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 将套接字绑定到服务器地址
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Waiting for connections...\n");
// 初始化客户端套接字数组
for (i = 0; i < MAX_CLIENTS; i++) {
client_socket[i] = 0;
}
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 将服务器套接字添加到集合
FD_SET(server_fd, &readfds);
max_sd = server_fd;
// 将客户端套接字添加到集合
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
// 如果套接字有效,将其添加到集合
if (sd > 0) {
FD_SET(sd, &readfds);
}
// 更新最大文件描述符
if (sd > max_sd) {
max_sd = sd;
}
}
// 使用 select 监听活动套接字
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("select failed");
}
// 如果服务器套接字有活动,表示有新连接
if (FD_ISSET(server_fd, &readfds)) {
int new_socket;
struct sockaddr_in client_addr;
int addrlen = sizeof(client_addr);
// 接受新连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&client_addr, (socklen_t *)&addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
// 打印客户端的主机名和地址
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("New connection: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// 将新连接添加到客户端套接字数组
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
break;
}
}
}
// 检查客户端套接字的活动
for (i = 0; i < MAX_CLIENTS; i++) {
sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
// 读取客户端发送的消息
if ((valread = read(sd, buffer, BUFFER_SIZE)) == 0) {
// 客户端断开连接
getpeername(sd, (struct sockaddr *)&server_addr, (socklen_t *)&addrlen);
printf("Client disconnected: %s:%d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
close(sd);
client_socket[i] = 0;
} else {
// 打印客户端的主机名和消息
buffer[valread] = '\0';
printf("From %s:%d: %s\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buffer);
}
}
}
}
return 0;
}
c#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, epoll_fd, event_count, i, client_fd;
struct epoll_event event, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
struct sockaddr_in server_addr, client_addr;
socklen_t addrlen = sizeof(client_addr);
// 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址和端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 将套接字绑定到服务器地址
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Waiting for connections...\n");
// 创建 epoll 实例
if ((epoll_fd = epoll_create1(0)) < 0) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
// 将服务器套接字添加到 epoll 实例的事件集合中
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
perror("epoll_ctl failed");
exit(EXIT_FAILURE);
}
while (1) {
// 等待事件发生
event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (event_count < 0) {
perror("epoll_wait failed");
exit(EXIT_FAILURE);
}
// 处理所有发生的事件
for (i = 0; i < event_count; i++) {
// 如果是服务器套接字上的事件,表示有新连接
if (events[i].data.fd == server_fd) {
// 接受新连接
if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addrlen)) < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
// 打印客户端的主机名和地址
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("New connection: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// 将新连接添加到 epoll 实例的事件集合中
event.events = EPOLLIN;
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
perror("epoll_ctl failed");
exit(EXIT_FAILURE);
}
}
// 如果是客户端套接字上的事件,表示有数据到达
else {
client_fd = events[i].data.fd;
// 读取客户端发送的消息
int valread = read(client_fd, buffer, BUFFER_SIZE);
if (valread <= 0) {
// 客户端断开连接
getpeername(client_fd, (struct sockaddr *)&client_addr, &addrlen);
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("Client disconnected: %s:%d\n", client_ip, ntohs(client_addr.sin_port));
// 从 epoll 实例的事件集合中移除客户端套接字
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_fd, NULL);
close(client_fd);
} else {
// 打印客户端的主机名和消息
buffer[valread] = '\0';
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
printf("从 %s:%d 收到消息: %s\n", client_ip, ntohs(client_addr.sin_port), buffer);
}
}
}
}
return 0;
}
本文作者:御坂19327号
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!