TCP/IP 网络编程 (三)
server端未处理高并发请求通常採用例如以下方式:
- 多进程:通过创建多个进程提供服务
- 多路复用:通过捆绑并统一管理
I/O
对象提供服务 - 多线程:通过生成和客户端等量的线程提供服务
多进程server端
#include <unistd.h>
pid_t fork(); // 成功返回进程 ID, 失败返回-1
fork
函数将创建调用的函数副本。子进程将使用新的内存空间复制当前函数的环境。
- 父进程:函数返回子进程
ID
- 子进程:函数返回 0
能够理解为调用该函数之后将存在两个pid_t
,分别存在父子进程中,因此它们将会依据不同的函数值运行对应的进程代码。
在调用函数前的全部变化都会在子进程中保持一致。
调用函数之后的变化将不会影响彼此,由于它们全然不相干。尽管能够通过进程间通信交换信息。
僵尸(zombie)进程
向exit
函数传递的參数值和return
语句返回的值会传递给操作系统。
操作系统不会销毁子进程。而是将这些值传递给产生该子进程的父进程。处在这样的状态下的进程就是僵尸进程。
exit(0)
表示程序正常, exit(1)/exit(-1)
表示程序异常退出;
exit()
结束当前进程/当前程序/。在整个程序中,仅仅要调用 exit
,就结束.
exit(0)
:正常运行程序并退出程序;
exit(1)
:非正常运行导致退出程序;
return()
:返回函数。若在main
主函数中,则会退出函数并返回一值。能够写为return(0)
。或return 0
。
exit
表示进程终结,无论是在哪个函数调用中。即使还存在被调函数。
return
表示函数返回,假设是在主函数中意味着进程的终结。假设不是在主函数中那么会返回到上一层函数调用。
具体说:
return
返回函数值。是关键字;exit
是一个函数。return
是语言级别的。它表示了调用堆栈的返回;而exit
是系统调用级别的,它表示了一个进程的结束。return
是函数的退出(返回);exit
是进程的退出。return
是C
语言提供的,exit
是操作系统提供的(或者函数库中给出的)。return
用于结束一个函数的运行,将函数的运行信息传出给其它调用函数使用;exit
函数是退出应用程序,删除进程使用的内存空间。并将应用程序的一个状态返回给OS
,这个状态标识了应用程序的一些运行信息,这个信息和机器和操作系统有关。通常是0
为正常退出。非0
为非正常退出。- 非主函数中调用
return
和exit
效果非常明显。可是在main
函数中调用return
和exit
的现象就非常模糊,多数情况下现象都是一致的。
销毁僵尸进程
利用 wait 函数
#include <sys/wait.h>
pid_t wait(int * statloc); // 成功返回终止的子进程 ID。失败返回 -1
当有子进程终止时,子进程终止时传递的返回值将保存在该函数參数所指内存空间,參数指向的单元中还包括其它信息。须要使用宏进行分离。
通过调用该函数之前终止的子进程相关信息将保存在參数变量中,同一时候。相关子进程被全然销毁。调用是假设没有已终止的进程,那么程序将会堵塞直到有子进程终止。
WIFEXITED
: 子进程正常终止时返回真WEXITSTATUS
: 返回子进程的返回值
也就是说。向wait
函数传递变量status
的地址时,调用wait
函数后应编写例如以下代码:
if(WIFWXITED(status))
{
puts("Normal termination!");
printf("Child pass num: %d", WEXITSTATUS(status));
}
利用 waitpid 函数
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * statloc, int options); // 同上
~ pid: 等待终止的目标子进程的ID, 若传递-1,则等效于 wait, 能够等待随意子进程终止
~ statloc: 同上
~ options: 传递常量 WNOHANG, 即使没有终止的子进程也不会进入堵塞状态,而是返回0并退出
代码演示样例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid = fork();
if(pid == 0)
{
sleep(15);
return 34;
}
else
{
while(!waitpid(-1, &status, WNOHANG)) //假设没有进程终止,就循环等待
{
sleep(3);
puts("sleep 3 sec.");
}
if(WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}
信号处理
子进程终止的识别主题是操作系统,因此在子进程终止的时候由操作系统将这些信息通知忙碌的父进程,父进程停下手上的工作处理相关事宜。
为此,我们引入信号处理。此处的“信号”是指在特定时间发生时由操作系统向进程发送的消息。为了响应该消息,运行与消息相关的自己定义操作的过程称为“处理”或“信号处理”。
信号和 signal 函数
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int); // 在产生信号时调用。返回之前注冊的函数指针
发生第一个參数代表的情况时。调用第二个參数所指的情况。
第一个參数可能对应的常数:
SIGALRM
: 已到通过alarm
函数注冊的时间SIGINT
: 输入CTRL + C
SIGCHLD
: 子进程终止
代码演示样例:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig) // 信号处理器
{
if(sig == SIGALRM)
puts("Time out!");
alarm(2);
}
void keycontrol(int sig)
{
if(sig == SIGINT)
puts("Ctrl + C Pressed");
}
int main()
{
int i;
signal(SIGALRM, timeout); // 注冊处理函数
signal(SIGINT, keycontrol);
alarm(2); // unistd.h
for(i=0; i < 3; i++)
{
puts("wait ... ");
sleep(100);
}
return 0;
}
利用 sigaction 处理信号
#include <signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact); // 成功返回0, 失败返回-1
~ signo: 传递信号信息
~ act: 对应于第一个參数的信号处理函数信息
~ oldact: 通过此參数获取之前注冊注冊的信号处理函数指针。不须要则传递0
struct sigaction
{
void (*sa_handler)(int); // 信号处理的函数指针
sigset_s sa_mask; // 用于指定信号相关的选项和特性
int sa_flags;
}
使用上和之前的signal
没有明显差别。
struct sigaction act;
act.sa_handler=timeout;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM, &act, 0);
...
利用信号处理技术消灭僵尸进程
代码演示样例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Remove proc id : %d \n", id);
printf("Child send : %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char * argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if(pid == 0)
{
puts("Hi! I'am child proc");
sleep(10);
return 12;
}
else
{
printf("Child proc id : %d \n", pid);
pid = fork();
if(pid == 0)
{
puts("Hi! I'am child proc");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id : %d \n", pid);
for(i=0; i < 5; i++)
{
puts("wait ... ");
sleep(5);
}
}
}
return 0;
}
为了等待SIGCHLD
信号,父进程共暂停5次,每次间隔5秒。
发生信号时。父进程将被唤醒,因此实际暂停不到25秒。
基于多任务的并发server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void errorhandling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
errorhandling("bind() error");
puts("bind sucess");
if(listen(serv_sock, 5) == -1)
errorhandling("listen() error");
puts("listen success");
while(1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock == -1)
continue;
else
puts("new client connected ..");
pid = fork();
if(pid == -1)
{
puts("pid = -1");
close(clnt_sock);
continue;
}
if(pid == 0)
{
puts("child proc.");
close(serv_sock);
while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected ..");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("remove proc id : %d \n", pid);
}
void errorhandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
通过 fork 函数拷贝文件描写叙述符
上述演示样例中父进程将两个文件描写叙述符(server套接字和客户端套接字)复制给子进程。
实际上仅仅是复制了文件描写叙述符,没有复制套接字。
由于套接字并不是进程全部—严格来说,套接字属于操作系统—仅仅是进程拥有代表对应套接字的文件描写叙述符。
如上图所看到的,仅仅有两个文件描写叙述符都终止后才干销毁套接字。即使子进程销毁了与客户端的套接字文件描写叙述符也不能全然销毁套接字。因此,调用fork
函数之后,要将无关的套接字文件描写叙述符关掉。例如以下图所看到的:
切割 TCP 的 I/O 程序
切割模型例如以下:
在客户端中将读写分离。这样就不用再写之前等待读操作的完毕。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char * message);
void read_routine(int sock, char *message);
void write_routine(int sock, char *message);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc != 3)
{
printf("Usage : %s <IP> <port> \n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error ");
pid = fork();
if(pid == 0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while(1)
{
int str_len = read(sock, buf, BUF_SIZE);
if(str_len == 0)
return ;
buf[str_len] = 0;
printf("Message from server : %s", buf);
}
}
void write_routine(int sock, char *buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
进程间通信
通过管道实现进程间通信
管道属于操作系统资源,因此。两个进程通过操作系统提供的内存空间进行通信。以下是创建管道的函数:
#include <unistd.h>
int pipe(int filedes[2]) // 成功返回0, 失败返回-1
~ filedes[0]: 通过管道接收数据时使用的文件描写叙述符
~ filedes[1]: 通过管道发送数据时使用的文件描写叙述符
演示样例代码:
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
int fds[2];
char str[] = "Who are you?";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds);
printf("fds[0] : %d, fsds1[1] : %d\n", fds[0], fds[1]);
pid = fork();
if(pid == 0)
{
printf("child proc : %d \n", pid);
read(fds[0], buf, BUF_SIZE);
puts(buf);
}
else
{
printf("parent proc : %d \n", pid);
write(fds[1], str, sizeof(str));
}
return 0;
}
上述代码的示比例如以下:
通过管道进行进程间的双向通信
模型例如以下:
这里有一个问题,“向管道中传递数据的时候。先读的进程会把数据读走”。
简而言之,数据进入到管道中就成为无主数据。无论谁先读取数据都能够将数据读走。
综上所述,使用一个管道实现双向通道并不是易事。由于须要预測并控制通信流,这是不现实的。因此能够使用两个管道实现双向通信。模型例如以下:
#include <stdio.h>
#include <unistd.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
int fds1[2], fds2[2];
char str1[] = "Who are you?
";
char str2[] = "Thank you!";
char buf[BUF_SIZE];
pid_t pid;
pipe(fds1);
pipe(fds2);
printf("fds1[0] : %d, fds1[1] : %d\n", fds1[0], fds1[1]);
printf("fds2[0] : %d, fds2[1] : %d\n", fds2[0], fds2[1]);
pid = fork();
if(pid == 0)
{
printf("child proc : %d \n", pid);
write(fds1[1], str1, sizeof(str1));
read(fds2[0], buf, BUF_SIZE);
printf("Child proc output: %s \n", buf);
}
else
{
printf("parent proc : %d \n", pid);
read(fds1[0], buf, BUF_SIZE);
printf("Parent proc output: %s \n", buf);
write(fds2[1], str2, sizeof(str2));
sleep(3);
}
return 0;
}
运用进程间通信
代码演示样例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void errorhandling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
int fds[2];
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
errorhandling("bind() error");
puts("bind sucess");
if(listen(serv_sock, 5) == -1)
errorhandling("listen() error");
pipe(fds);
pid = fork();
if(pid == 0)
{
FILE * fp = fopen("echomsg.txt", "wt");
char msgbuf[BUF_SIZE];
int i, len;
for(i=0; i<10; i++)
{
len = read(fds[0], msgbuf, BUF_SIZE);
fwrite((void *)msgbuf, 1, len, fp);
}
fclose(fp);
return 0;
}
while(1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if(clnt_sock == -1)
continue;
else
puts("new client connected ..");
pid = fork();
if(pid == 0)
{
puts("child proc.");
close(serv_sock);
while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
{
write(clnt_sock, buf, str_len);
write(fds[1], buf, str_len);
}
close(clnt_sock);
puts("client disconnected ..");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("remove proc id : %d \n", pid);
}
void errorhandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
上述代码涉及到的模型例如以下:
I/O 复用
这里我们讨论使用 I/O
复用解决每一个客户端请求都创建进程的资源消耗弊端。
运用select
函数是最具有代表性的实现复用server端方法。使用该函数的时候能够将多个文件描写叙述符集中到一起进行监视。
- 是否存在套接字接收数据?
- 无需堵塞数据传输的套接字有哪些?
- 哪些套接字发生了异常?
我们将监视项称为事件。发生了监视项对应的情况时,称“发生了事件”。
select
函数非常难使用,可是为了实现I/O
复用server端,我们应该掌握该函数。并运用到套接字编程中。觉得“select
函数时I/O
复用的全部内容”并不为过。
设置文件描写叙述符
将要监视的套接字集合在一起,集中式也要依照监视项(接收、传输、异常)进行区分。使用fd_set
数组变量运行此项操作。
在数组中注冊或者更改值的操作应该由下列宏完毕:
FD_ZERO(fd_set * fdset): 全部位初始化为0
FD_SET(int fd, fd_set * fdset): 注冊文件描写叙述符fd 的信息
FD_CLR(int fd, fd_set * fdset): 清楚文件描写叙述符的信息
FD_ISSET(int fd, fd_set * fdset): 是否监视
上述定义可在以下直观看到效果:
设置监视范围及超时
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * excepyset, const struct timeval * timeout); // 成功时返回大于0的值。 失败返回-1
~ maxfd: 监视对象文件描写叙述符的数量
~ readset: 是否存在待读取数据
~ writeset: 是否可传输无堵塞数据
~ exceptset: 是否发生异常
~ timeout: 调用该函数之后,为防止陷入无限堵塞的状态,传递超时信息
返回值: 错误发生返回-1, 超时返回0,因发生关注的事件返回时。返回大于0的值,该值是发生事件的文件描写叙述符数
“文件描写叙述符的监视范围?”
函数要求通过第一个參数传递监视对象文件描写叙述符的数量。因此须要得到注冊在fdset
变量中的文件描写叙述符数。
但每次新建文件描写叙述符时。其值都会加1,故将最大的文件描写叙述符加1再传递到函数就可以。加1是由于文件描写叙述符的值从0開始。
“怎样设置超时时间?”
struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
}
函数仅仅有在监视的文件描写叙述符发生变化时才返回。假设未发生变化就会进入堵塞状态。指定超时时间就是为了防止这样的情况的发生。
即使文件描写叙述符未发生变化,仅仅要到了指定时间函数也会返回。当然,返回值是0。假设不设置超时,传递NULL
就可以。
调用函数后查看结果
假设函数返回值大于0,说明对应数量的文件描写叙述符发生变化。
文件描写叙述符变化是指监视的文件描写叙述符发生了对应的监视事件
那么,怎样得知哪些文件描写叙述符发生了变化呢?
向函数传递的第二个到第四个參数传递的fd_set
变量将发生例如以下图所看到的的变化:
函数调用之后,向其传递的fd_set
变量将发生变化,原来为1的全部位将变成0,但发生变化的位除外。换句话说就是。调用之后,发生变化的文件描写叙述符的位将为1。
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads);
while(1)
{
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
result = select(1, &temps, 0, 0, &timeout);
if(result == -1)
{
puts("select() error");
break;
}
else if(result == 0)
{
puts("Time out");
}
else
{
if(FD_ISSET(0, &temps))
{
str_len = read(0, buf, BUF_SIZE);
buf[str_len] = 0;
printf("message from console : %s", buf);
}
}
}
return 0;
}
实现I/O复用server端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void errorhandling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int str_len, fd_max, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
errorhandling("bind() error");
puts("bind sucess");
if(listen(serv_sock, 5) == -1)
errorhandling("listen() error");
puts("listen success");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while(1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
{
errorhandling("select() error");
break;
}
if(fd_num == 0)
{
puts("Time out");
continue;
}
for(i=0; i < fd_max + 1; i++)
{
if(FD_ISSET(i, &cpy_reads))
{
if( i == serv_sock) //continue request
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client : %d \n", clnt_sock);
}
else // read message
{
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0) // close request
{
FD_CLR(i, &reads);
close(i);
printf("closed client : %d \n", i);
}
else
{
write(i, buf, str_len); //echo
}
}
}
}
}
close(serv_sock);
return 0;
}
void errorhandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
多种 I/O 函数
Linux 中的 send & recv
#include <sys/socket.h>
ssize_t send(int sockfd, const void * buf, size_t nbytes, int flags); // 成功返回发送的字节数。 失败返回-1
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
MSG_OOB: 发送紧急数据
带外数据概念实际上是向接收端传送三个不同的信息:
- 发送端进入紧急模式这个事实。接收进程得以通知这个事实的手段不外乎
SIGURG
信号或select
调用。本通知在发送进程发送带外字节后由发送端
TCP
马上发送。即使往接收端的不论什么数据发送因流量控制而停止了。TCP
仍然发送本通知。本通知可能导致接收端进入某种特殊处理模式,以处理接收的不论什么后继数据。
- 带外字节的位置。也就是它相对于来自发送端的其余数据的发送位置:带外标记。
- 带外字节的实际值。
既然
TCP
是一个不解释应用进程所发送数据的字节流协议。带外字节就能够是不论什么8位值。
对于TCP
的紧急模式。我们能够觉得URG
标志时通知(信息1),紧急指针是带外标记(信息2),数据字节是其本身(信息3)。
与这个带外数据概念相关的问题有:
- 每一个连接仅仅有一个
TCP
紧急指针; - 每一个连接仅仅有一个带外标记;
- 每一个连接仅仅有一个单字节的带外缓冲区(该缓冲区仅仅有在数据非在线读入时才需考虑)。假设带外数据时在线读入的,那么当心的带外数据到达时。先前的带外字节字节并未丢失,只是他们的标记却因此被新的标记代替而丢失了。
带外数据的一个常见用途体如今rlogin
程序中。当客户中断运行在server主机上的程序时。server须要告知客户丢弃全部已在server排队的输出,由于已经排队等着从server发送到客户的输出最多有一个窗体的大小。
server向客户发送一个特殊字节。告知后者清刷全部这些输出(在客户看来是输入),这个特殊字节就作为带外数据发送。
客户收到由带外数据引发的SIGURG
信号后。就从套接字中读入直到碰到带外数据。
客户收到由带外数据引发的SIGURG
信号后,就从套接字中读入直到碰到带外标记,并丢弃到标记之前的全部数据。
这样的情形下即使server相继地高速发送多个带外字节,客户也不受影响。由于客户仅仅是读到最后一个标记为止,并丢弃全部读入的数据。
总之。带外数据是否实用取决于应用程序使用它的目的。假设目的是告知对端丢弃直到标记处得普通数据,那么丢失一个中间带外字节及其对应的标记不会有什么不良后果。可是假设不丢失带外字节本身非常重要,那么必须在线收到这些数据。另外。作为带外数据发送的数据字节应该差别于普通数据,由于当前新的标记到达时,中间的标记将被覆写,从而其实把带外字节混杂在普通数据之中。
举例来说,telnet
在客户和server之间普通的数据流中发送telnet
自己的命令。手段是把值为255
的一个字节作为telnet
命令的前缀字节。(值为255的单个字节作为数据发送须要2个相继地值为255的字节。
)这么做使得telnet
能够区分其命令和普通用户数据,只是要求客户进程和server进程处理每一个数据字节以寻找命令。
除紧急指针(URG
指针)指向的一个字节外,数据接收方将通过调用经常使用输入函数读取剩余部分。
检查输入缓冲
设置MSG_PEEK
选项并调用recv
函数之后,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与MSG_DONTWAIT
合作。用于调用非堵塞方式验证待读取数据存在与否。
readv & writev 函数
对数据进行整合传输及发送的函数
通过writev
函数能够将分散保存在多个缓冲中的数据一并发送。适当使用这两个函数能够降低I/O
函数的调用次数。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec * iov, int iovcnt);
~ filedes: 数据传输对象的套接字文件描写叙述符
~ iov: iovec结构体数组的地址,结构体中包括待发送数据的位置和大小信息
~ iovcnt: 第二个參数的数组长度
struct iovec
{
void * iov_base; // 缓冲地址
size_t iov_len; // 缓冲大小
}
关系模型例如以下:
#include <stdio.h>
#include <sys/uio.h>
int main()
{
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
int str_len;
vec[0].iov_base = buf1;
vec[0].iov_len = 3;
vec[1].iov_base = buf2;
vec[1].iov_len = 4;
str_len = writev(1, vec, 2);
puts("");
printf("Writen bytes : %d \n", str_len);
return 0;
}
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec * iov, int iovcnt);
readv.c