您的位置:首页 > 理论基础 > 计算机网络

Linux 网络编程笔记(2)——socket 编程

2017-05-28 00:32 381 查看
socket 可以看作是用户进程与内核网络协议栈的编程接口

既可以用于本机的进程间通信,还可以用于网络上不同主机的进程间通信

listenfd 是被动套接字,可以用来接受连接。

accept 从已完成连接队列中取出一个队列头,得到一个新套接字 connfd。

connfd 是主动套接字,不能用来接受连接。

服务端和客户端的 ip 和 port 四元组表示 一个连接。

REUSEADDR 可以解决“关闭一个服务器后,由于 TIME_WAIT 无法马上重启服务器的问题”。在 TIME_WAIT 还没消失的时候允许重启。

流协议与粘包问题

tcp 是字节流协议,无边界。

接收方不能保证读操作的时候能返回多少个字节。

UDP 是基于消息的传输服务,传输的是报文,有边界。

能保证接收方每次返回一条消息。

几种产生可能性

应用进程缓冲区的数据拷贝到套接口发送缓冲区时,如果应用层缓冲区一条消息大小超过套接口发送缓冲区的大小时,就有可能产生粘包问题。(消息被分割)

TCP 传输的段有最大限制(MSS),超过 MSS 就会被分割 。

链路层也有最大传输限制,在 IP 网络层会分组。

TCP 流量控制、拥塞控制等。

TCP 延迟发送机制。

粘包解决方案

本质上是要在应用层维护消息与消息的边界。

定长包

包尾加上 \r\n (ftp)(要考虑消息本身具有 \r\n 这种情况)

包头加上包体长度

更复杂的应用层协议

readn()
writen()
的封装,封装后只有接收到指定字节数才会跳出循环,可用于发送定长包(比方说不管数据多长,一律发送 1024 个字节,有可能浪费网络资源)。

ssize_t readn(int fd, void *buf, size_t count)
{
size_t nleft = count;
ssize_t nread;
char *bufp = (char*)buf;
while (nleft > 0)
{
if (nread = read(fd, bufp, nleft) < 0)
{
if (errno = EINTR) // 被中断
continue;
else
return -1;   // 其他错误
}
else if (nread == 0)
{
return count - nread; // 结束,返回读取到的字节数
}
else
{
bufp += nread;
nleft -= nread;
}
}
return count;
}

ssize_t writen(int fd, const char *buf, size_t count)
{
size_t nleft = count;
ssize_t nwritten;
char *bufp = (char*)buf;
while (nleft > 0)
{
if (nwritten = write(fd, bufp, nleft) < 0)
{
if (errno = EINTR)
continue;
else
return -1;
}
else if (nwritten == 0)
continue;
else
{
bufp += nwritten;
nleft -= nwritten;
}
}
}


可以自定义一种协议,包头 4 个字节表示包体长度(作为定长包的长度),后面的字节作为包体。

struct packet
{
int len;
chat buf[1024];
}
//...
struct packet sendbuf;
//...
struct packet recvbuf;


发送前,先用
n = strlen(sendbuf.buf); sendbuf.len = htol(n)
填充
sendbuf.len
(要注意字节序的问题)

然后再
writen(sock, sendbuf, 4+n)
n 为包体长度。

MSG_PEEK 读取数据但不清除缓存

read_peek()
封装了 MSG_PEEK

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
while(1)
{
int ret = recv(sockfd, buf, len, MSG_PEEK);
if(ret == -1 && errno == EINTR)
continue;
else
return ret;
}
}


read_peek()
read_n()
实现

readline()
:先
read_peek()
看有没有’\n’,有的话看在第几个,然后只读到这个字节。

ssize_t readline(int socket, void *buf, size_t maxline)
{
int ret;
int nread;
char *bufp;
int nleft = maxline;
while(1)
{
ret = recv_peek(socket, bufp, nleft); // 偷窥数据但不从缓冲区清除
// ret 为缓冲区中的数据量
if(ret < 0)
return ret;
else if(ret == 0)
return ret;
nread = ret;
for(int i = 0; i < nread; ++i)
{
if(bufp[i] == '\n')     // 有 '\n',在第 i+1 位
{
readn(socket, bufp, i+1);
if(ret != i+1)      // 没有读取到 i+1 个字节的数据
exit(EXIT_FAILURE;)
return ret;
}
}
// 以下为没 '\n' 的处理方法
nread = ret;
if(nread > nleft)
exit(EXIT_FAILURE);
ret = readn(socket, bufp, nread);
if(ret != nread)
exit(EXIT_FAILURE);
bufp += nread;
nleft -= nread;

}
}


recv()
read()
的区别

ssize_t read(int fd, void *buf, size_t count)


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


read()
处理的 fd 可以是任何 fd,
recv()
的 fd 是 sockfd。

recv()
多出来一个参数 flags 可以传入 MSG_PEEK 等参数。

僵尸进程

四种僵尸进程避免方式:

1.wait和waitpid函数
2.signal安装处理函数(交给内核处理)
3.signal忽略SIGCHLD信号(交给内核处理)
4.fork两次


pid_t wait(int *status)
只能阻塞,一旦 wait 之后,父进程将会阻塞自己直到子进程结束运行。当我们不关心子进程的退出状态,我们可以传入空指针。

pid_t waitpid(pid_t pid,int *status,int options)


pid_t

pid > 0 时,只等待进程 ID 等于 pid 的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束, waitpid就会一直等下去。

pid = -1时,等待任何一个子进程退出,没有任何限制,此时waitpid 和 wait的作用一模一样。   

pid = 0 时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会对它做任何理睬。

pid<-1 时,等待一个指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

options

Linux 中只支持 WNOHANG(不挂起、非阻塞) 和 WUNTRACED

可以往一个已经接收 FIN 的套接字中写,接收到 FIN 仅仅代表对方不再发送数据。

在收到 RST 段之后,如果再调用 write 就会产生 SIGPIPE 信号, 对于这个信号的处理我们通常忽略即可。(对端 close 之后,如果本端再 write,第一次会产生 RST,第二次产生 SIGPIPE 信号)

signal(SIGPIPE, SIG_IGN);


五种 I/O 模型

1.阻塞 I/O

2.非阻塞I/O

3.I/O多路复用

4.信号驱动I/O(得到信号时,仅仅表明有数据来,应用程序还要recv)

5.异步I/O(用户得到信号时,内核已经把数据推到了用户空间)

信号是异步处理的一种方式。

UDP

无连接

基于消息的数据传输服务

不可靠

一般情况 UDP 更高效

UDP 不需要 bind,而是再第一次 sendto 和 recvfrom 的时候自动绑定
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: