忧郁的大能猫
好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
套接字是通信端点的抽象。套接字在Linux上也是通过文件实现的,所以传统的write和read同样适用于套接字。
不同的 CPU 中,4 字节整数值1在内存空间保存方式是不同的。
* 大端序(Big Endian):低地址存高位
* 小端序(Little Endian):低地址存低位
在通过网络传输数据时必须约定统一的方式,这种约定被称为网络字节序,统一为大端序。即,先把数据数组转化成大端序格式再进行网络传输。
帮助转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);其中:
htons 的 h 代表主机(host)字节序。
htons 的 n 代表网络(network)字节序。
s 代表 short
l 代表 long
将字符串形式的IP地址转换成整数型的IP地址
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);inet_aton 函数与 inet_addr 函数在功能上完全相同,只不过该函数用了 in_addr 结构体,且使用频率更高
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);还有一个函数,与 inet_aton() 正好相反,它可以把网络字节序整数型IP地址转换成我们熟悉的字符串形式
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr);int opts = fcntl(st, F_GETFL); // F_GETFL F_SETFL
opts = opts | O_NONBLOCK; //设置nobloking
fcntl(st, F_SETFL, opts)利用域名获取IP地址
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);利用IP地址获取域名
#include <netdb.h>
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);| 分类 | 对比维度 | TCP | UDP |
|---|---|---|---|
| 协议基础与通信模型 | 全称 | Transmission Control Protocol | User Datagram Protocol |
| 协议层次 | 传输层(运行于 IP 之上) | 传输层(运行于 IP 之上) | |
| Socket 类型 | SOCK_STREAM(字节流套接字) | SOCK_DGRAM(数据报套接字) | |
| 通信模型 | 面向连接,一对一全双工 | 无连接,支持一对一、一对多、多对多 | |
| 广播支持 | 不支持 | 支持 | |
| 组播(多播)支持 | 不支持 | 支持 | |
| 连接机制与状态维护 | 连接建立 | 需要三次握手 | 无连接,直接发送 |
| 连接断开 | 需要四次挥手 | 无连接概念 | |
| 连接状态维护 | 需要维护大量连接状态信息 | 无状态,不维护 | |
| 服务器重启影响 | 连接断开,客户端必须重新连接 | 通常无需重连,可继续收发报文 | |
| 系统资源开销 | CPU 和内存开销较高 | CPU 和内存开销很低 | |
| 可靠性保障 | 可靠性模型 | 可靠传输,保证交付 | 尽力而为,不保证交付 |
| 确认机制 (ACK) | 有,确认数据到达 | 无 | |
| 超时重传 | 有,自动补发丢失数据 | 无,默认不重传 | |
| 快速重传 | 有,发现丢包后立即补发 | 无 | |
| 顺序保证 | 严格保证按序交付 | 不保证,可能乱序到达 | |
| 重复数据处理 | 自动去重 | 不处理,可能收到重复包 | |
| 错误恢复 | 协议自动处理 | 应用层自行处理 | |
| 传输控制与效率 | 滑动窗口机制 | 有,提高网络利用率 | 无 |
| 流量控制 | 有,防止接收方被压垮 | 无 | |
| 拥塞控制 | 有,慢启动、拥塞避免等 | 无 | |
| 网络适应能力 | 强,动态调整发送速率 | 弱,不关心网络状态 | |
| 头部长度 | 20~60 字节 | 固定 8 字节 | |
| 协议复杂度 | 高,实现复杂 | 低,实现简单 | |
| 传输效率 | 较低,可靠性带来额外开销 | 较高,轻量快速 | |
| 实时性 | 一般,重传和拥塞控制会引入延迟 | 很好,无阻塞发送 | |
| 数据特性与开发复杂度 | 数据边界 | 字节流,无消息边界,需处理粘包/拆包 | 保留消息边界,每个数据报独立 |
| 分片/重组 | 协议自动完成,对应用透明 | IP 层可能分片,应用层常需控制报文大小或实现重组 | |
| 应用开发难度 | 较低,协议解决大部分网络问题 | 较高,需自行处理可靠性、排序、丢包、分片等 | |
| 适用场景 | 适合场景 | 网页、文件传输、邮件、远程登录等对可靠性要求高的场景 | 流媒体、VoIP、视频会议、DNS、局域网应用、广播/多播、周期性不重要消息 |
〇、连接管理,建立连接双方各自维护状态
- 三次握手:在传输数据前,通过三次交互来同步双方的初始序列号,并确认双方的收发能力都正常,为可靠传输建立了一条有状态的通道。连接的建立和断开本身也保证了通信双方状态的同步。
一、序列号与数据去重、排序。确保数据不重、不漏、不乱
- 字节级序列号:
TCP 将数据看作无结构的字节流,并为每个字节分配一个唯一的序列号。报文段头部的序列号,就是该段数据第一个字节的编号。
- 接收方排序与去重:
接收方依据序列号,对可能乱序到达的报文段重新排列组合,还原成原始顺序的数据流。同时,通过序列号可以轻松识别并丢弃重复收到的报文段。
二、确认与自动重传机制,确保数据不会丢失
- 确认应答(ACK):
接收方在成功收到数据后,会向发送方回复一个确认报文。发送方只有在收到这个 ACK 后,才会认为数据已成功送达,并从发送缓冲区中清除该数据。
- 超时重传:
发送方每发出一个报文段,就会启动一个定时器。如果在定时器超时后仍未收到 ACK,就认为该报文段丢失,并自动重传。TCP 会通过算法动态计算这个超时时间,以适应网络延迟的变化。
- 快速重传:
一种更高效的重传方式。当发送方连续收到3个或以上相同的冗余 ACK(例如,接收方一直说“我还在等序号 x”,说明x之后的3个包都到了,但是x还没有到呢),它就能推断报文段 X 已丢失,无需等待定时器超时,会立即重传该丢失的报文段。
三、校验和机制,确保数据不出错
- TCP 头部包含一个 16 位的校验和字段。发送方将伪首部、TCP 首部和数据部分进行计算填充。接收方收到报文后会重新计算校验和并进行比对,如果不匹配,则判定数据在传输中出错,直接丢弃该报文段,并触发后续的重传机制。
四、流量控制
目的是防止“发送方”压垮“接收方”,确保数据不因接收端缓冲区溢出而丢失。
- 滑动窗口协议:
接收方每次回复 ACK 时,都会在 TCP 头部的“窗口大小”字段里声明自己当前还能接收多少数据。发送方根据这个窗口大小来动态调整发送速率和发送量,确保发送的数据不会超过接收方的处理能力。
五、拥塞控制
目的是防止“发送方”压垮“整个网络”,避免因网络拥堵导致的大规模丢包。
- TCP 维护一个“拥塞窗口”变量,通过四种核心算法动态调整发送速率:
- 慢启动:连接开始时,拥塞窗口很小,数据发送速率指数级增长,试探网络的承受能力。
- 拥塞避免:当拥塞窗口增长到一定阈值后,转为线性缓慢增长,避免引发拥塞。
- 快重传与快恢复:配合快速重传机制,一旦检测到丢包,立刻将拥塞窗口减半,快速降低发送速率以缓解网络拥塞,然后进入拥塞避免阶段。
//=======cliet======
socket = socket(PF_INET,SOCK_STREAM)
connet(socket, address)
while(1)
{
write(socket, buf, buf_len)
read(socket, buf, buf_len)
}
close()
//======server=======
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
c_socket = accept(s_socket, client_address)
while(read(c_socket, buf, buf_len)!=0)
{
write(c_socket, buf, buf_len)
}
close(c_socket)
close(s_socket)s_socket = socket(PF_INET,SOCK_DGRAM)
bind(s_socket,address)
while(1)
{
str_len = recvfrom(s_socket, buf, buf_len, 0, &client_addr, addr_size);
sendto(s_socket, buf, buf_len, 0,&client_addr, addr_size);
}
close(s_socket)
sock = socket(PF_INET,SOCK_DGRAM)
bind(s_socket,address)
while(1)
{
sendto(sock, buf, buf_len, 0,&client_addr, addr_size);
recvfrom(sock, buf, buf_len, 0, &client_addr, addr_size);
}
close(sock)
网络模型需要的线程数量

这个方案不适合长连接,倒是很适合daytime这种write-only短连接服务
这种把业务逻辑隐藏在一个大循环中的做法其实不利于将来功能的扩展,
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
while(1)
{
c_socket = accept(s_socket, client_address)
while(read(c_socket, buf, buf_len)!=0)
{
write(c_socket, buf, buf_len)
}
close(c_socket)
}
close(s_socket)这种方案适合并发连接数不大的情况。至今仍有一些网络服务程序用这种方式实现,比如PostgreSQL和Perforce的服务端。这种方案适合“计算响应的工作量远大于fork()的开销”这种情况,比如数据库服务器。这种方案适合长连接,但不太适合短连接,因为fork()开销大于求解Sudoku的用时。
这种把业务逻辑隐藏在一个大循环中的做法其实不利于将来功能的扩展,
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
while(1)
{
c_socket = accept(s_socket, client_address)
pid = fork()
if(pid > 0 )
{
close(c_socket)
}else if(pid == 0)
{
close(s_socket)
while(read(c_socket, buf, buf_len)!=0)
{
write(c_socket, buf, buf_len)
}
close(c_socket)
}
}
close(s_socket)这是传统的Java网络编程方案thread-per-connection,在Java 1.4引入NIO之前,Java网络服务多采用这种方案。它的初始化开销比方案1要小很多,但与求解Sudoku的用时差不多,仍然不适合短连接服务。这种方案的伸缩性受到线程数的限制,一两百个还行,几千个的话对操作系统的scheduler恐怕是个不小的负担。
void *handle_clinet(void *arg )
{
c_socket = *((int *)arg)
while(read(c_socket, buf, buf_len)!=0)
{
write(c_socket, buf, buf_len)
}
}
pthread_t tid
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
while(1)
{
c_socket = accept(s_socket, client_address)
pthread_create(&tid, NULL, handle_client, (void *)&c_socket);
pthread_detach(tid);
}
close(s_socket)select要注意最大文件描述符是1024,超过了会崩溃的。
使用select可以将多个socket集中到一起统一监视,监视内容为:接收事件,非阻塞传输事件,异常
刚开始时把server_soket设置到fd_set文件描述符集合中,然后开始循环调用select去监视所有socet的变化,
当select返回值大于0,说明有变化,然后遍历fd_set的集合,判断哪些socket发生变化,
- 如果是server_socket发生变化则将accept到的cliet_socket设置到fd_set中
- 如果是cliet_socket发生变化则进行读写操作,若断开则FD_CLR()掉
fd_max=100
fd_set read_set
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
FD_ZERO(&read_set)
FD_SET(s_socket, &read_set)
while(1)
{
temp_read_set = read_set //每次都要复制
count = select(fd_max, temp_read_set, NULL, NULL, timeout )
if(count == 0) continue
for(i = 1, i < fd_max, i++)
{
if(FD_ISSET(i, &temp_read_set)) //是否有变化,在select的时候被设置
{
if(i==serv_sock)
{
c_socket = accept(s_socket, client_address)
FD_SET(c_socket, &read_set);
}else
{
ret = read(i, buf, buf_len)
if(ret > 0)
{
write(i, buf, buf_len)
}else
{
FD_CLR(i, &read_set);
close(i);
}
}
}
}
}
close(s_socket)select慢的原因:
1. 调用 select 函数后针对所有文件描述符的循环语句,即使只有一个socket改变了
2. 每次调用 select 函数时都需要向该函数传递fd集合(fd_set),涉及到向内核传递数据。
select 的兼容性比较高(windows和linux都可以),这样就可以支持很多的操作系统,不受平台的限制,使用select函数满足以下两个条件:
1. 服务器接入者少
2. 程序应该具有兼容性
下面是epoll函数的功能:
* epoll_create:创建保存epoll文件描述符的空间,everything is file,当然epoll的也不例外
* epoll_ctl:向空间注册并注销文件描述符
* epoll_wait:与 select 函数类似,等待文件描述符发生变化
select 函数中为了保存监视对象的文件描述符,直接声明了 fd_set 变量,但 epoll 方式下操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时用的函数就是 epoll_create。好处是不用在用户空间和内核空间传送数据了。
epoll流程:
1. epoll_create创建一个保存epoll文件描述符的空间
2. 动态分配内存,给将要监视的 epoll_wait,以读取所有发生事件的socket
3. 利用epoll_ctl添加server_socket的EPOLLIN事件
4. 利用epoll_wait来获取改变的文件描述符,把结果存在epoll_event类型的指针内存中
5. 处理发生事件的socket
6. 重复4-5步骤
EPOLL_SIZE = 100
struct epoll_event *events; //保存所有事件
struct epoll_event event;
s_socket = socket(PF_INET,SOCK_STREAM)
bind(s_socket,address)
listen(s_socket,queuelen)
// setnonblockingmode(s_socket);
ep_fd = epoll_create() //可以忽略这个参数,填入的参数为操作系统参考
events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events = EPOLLIN
event.data.fd = s_socket
epoll_ctl(ep_fd, EPOLL_CTL_ADD, s_socket, &event)
while(1)
{
count = epoll_wait(ep_fd, events, EPOLL_SIZE, -1); //-1是设置成永不超时
for(i = 1, i < count, i++)
{
fd = events[i].data.fd
if(fd == s_socket)
{
c_socket = accept(s_socket, client_address)
event.events=EPOLLIN // 设置监视类型
// event.events = EPOLLIN | EPOLLET; //改成边缘触发
// setnonblockingmode(c_socket); //将 accept 创建的套接字改为非阻塞模式
event.data.fd=c_socket
epoll_ctl(ep_fd, EPOLL_CTL_ADD, c_socket, &event)
}else
{
ret = read(i, buf, buf_len)
if(ret > 0)
{
write(fd, buf, buf_len)
}else
{
epoll_ctl(ep_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
/* ET触发
while(1)
{
ret = read(i, buf, buf_len)
if(ret > 0)
{
write(fd, buf, buf_len)
}else if(ret<0)
{
if(errno = EAGAIN) braek;
}
else
{
epoll_ctl(ep_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
*/
}
}
}
close(s_socket)
close(ep_fd)
free(events)
events = NULLepoll默认以水平触发方式工作,select也是以条件触发模式工作的。
两者区别:在于发生事件的时间点
* 水平触发:只要输入缓冲有数据就一直通知该事件
- 读
- 比如:服务器端输入缓冲收到50字节数据时,read了30字节,还剩20,仍会触发事件
- 缓冲区有数据就一直通知,没读完没关系。
- LT 为什么可以阻塞的read,read(fd, buf, 1024); 读一次就返回。 剩余数据:下次 epoll_wait 再通知
- 写
- 当网络不可写(write/send)时,说明发送缓冲区已经满了
- LT规则:只要可写,就一直通知。
* 边缘触发:输入缓冲收到数据时仅注册一次事件,即使输入缓冲中还留有数据
- 读
- read时缓存空,返回 -1,变量 errno 中的值变成 EAGAIN 时,说明没有数据可读
- 因为一次性要读清,必须使用非阻塞(Non-blocking)I/O ,要不你不知道读多少,否则可能再也收不到通知。
- 分离接收数据和处理数据的时间点,给服务端的实现带来很大灵活性。性能上使用得当会优于条件触发。
- 写
- 只在状态变化时通知一次。
- 写的时候都要一直操作send直到返回 EAGAIN 为止,否则可能丢失后续事件通知
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
ssize_t socket_recv(int st)
{
char buf[1024];
memset(buf, 0, sizeof(buf));
ssize_t rc = recv(st, buf, sizeof(buf), 0);
if (rc <= 0)
{
printf("recv failed %s\n", strerror(errno));
} else
{
printf("recv %s\n", buf);
send(st, buf, rc, 0);
}
return rc;
}
int socket_accept(int listen_st)
{
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
memset(&client_addr, 0, sizeof(client_addr));
int client_st = accept(listen_st, (struct sockaddr *) &client_addr, &len);
if (client_st < 0)
printf("accept failed %s\n", strerror(errno));
else
printf("accept by %s\n", inet_ntoa(client_addr.sin_addr));
return client_st;
}
void setnonblocking(int st) //将socket设置为非阻塞
{
int opts = fcntl(st, F_GETFL);
if (opts < 0)
{
printf("fcntl failed %s\n", strerror(errno));
}
opts = opts | O_NONBLOCK;
if (fcntl(st, F_SETFL, opts) < 0)
{
printf("fcntl failed %s\n", strerror(errno));
}
}
int socket_create(int port)
{
int st = socket(AF_INET, SOCK_STREAM, 0);
int on = 1;
if (setsockopt(st, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
{
printf("setsockopt failed %s\n", strerror(errno));
return 0;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(st, (struct sockaddr *) &addr, sizeof(addr)) == -1)
{
printf("bind port %d failed %s\n", port, strerror(errno));
return 0;
}
if (listen(st, 300) == -1)
{
printf("listen failed %s\n", strerror(errno));
return 0;
}
return st;
}
int main(int arg, char *args[])
{
if (arg < 2)
return -1;
int iport = atoi(args[1]);
int listen_st = socket_create(iport);
if (listen_st == 0)
return -1;
struct epoll_event ev, events[100]; //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
memset(&ev, 0, sizeof(ev));
memset(events, 0, sizeof(events));
int epfd = epoll_create(100); //建立epoll描述符
setnonblocking(listen_st); //把socket设置为非阻塞方式
ev.data.fd = listen_st; //设置与要处理的事件相关的文件描述符
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; //设置要处理的事件类型
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_st, &ev) == -1) //注册epoll事件
{
printf("epoll_ctl error , %s\n", strerror(errno));
return -1;
}
int st = 0;
while (1)
{
int nfds = epoll_wait(epfd, events, 100, -1); //阻塞, 等待epoll事件的发生,100是和events[100]相对应的
if (nfds == -1) //events存放出事的socket
{
printf("epoll_wait failed %s\n", strerror(errno));
break;
}
int i;
for (i = 0; i < nfds; i++) //遍历发生的事件
{
if (events[i].data.fd < 0)
continue;
if (events[i].data.fd == listen_st) //监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
{
st = socket_accept(listen_st);
if (st >= 0)
{
setnonblocking(st);
ev.data.fd = st;
ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; //设置要处理的事件类型??????
if (epoll_ctl(epfd, EPOLL_CTL_ADD, st, &ev) == -1)
{
printf("epoll_ctl error , %s\n", strerror(errno));
}
continue;
}
}
if (events[i].events & EPOLLIN) //socket收到数据 ??????
{
st = events[i].data.fd;
if (socket_recv(st) <= 0)
{
if (epoll_ctl(epfd, EPOLL_CTL_DEL, st, NULL) == -1)
{
printf("epoll_ctl error , %s\n", strerror(errno));
}
close(st);
}
}
if (events[i].events & EPOLLERR) //socket错误
{
st = events[i].data.fd;
if (epoll_ctl(epfd, EPOLL_CTL_DEL, st, NULL) == -1)
{
printf("epoll_ctl error , %s\n", strerror(errno));
}
close(st);
}
if (events[i].events & EPOLLHUP) //socket错误
{
st = events[i].data.fd;
if (epoll_ctl(epfd, EPOLL_CTL_DEL, st, NULL) == -1)
{
printf("epoll_ctl error , %s\n", strerror(errno));
}
close(st);
events[i].data.fd=-1;//epoll会自动从池子里清除掉
}
}
}
close(epfd);
return 0;
}这两种模式都是针对IO操作的,我的理解是Reactor只是告诉调用者什么时候事件到来,但是需要进行什么操作,需要调用者自己处理。
Preactor不是当事件到来时通知,而是针对此事件对应的操作完成时,通知调用者,一般通知方式都是异步回调。
举例,Reactor中注册读事件,那么文件描述符可读时,需要调用者自己调用read系统调用读取数据,若工作在Preactor模式,注册读事件,同时提供一个buffer用于存储读取的数据,那么Preactor通过回调函数通知用户时,用户无需在调用系统调用读取数据,因为数据已经存储在buffer中了。显然epoll是Reactor的。
Reactor 和 Proactor 的本质区别在于 IO 操作由谁完成。
- Reactor 是“就绪通知”模型,内核只通知文件描述符可读可写,应用程序再调用 read/write 完成数据拷贝,因此属于同步 IO。典型实现是 select、poll、epoll。
- Proactor 是“完成通知”模型,应用程序先发起异步 IO 请求,由内核完成等待数据和数据拷贝,IO 完成后再通知应用程序,因此属于真正的异步 IO。
- Linux 高性能服务器大多采用 epoll + Reactor,而 Windows 的 IOCP 则是典型的 Proactor 实现。
Reactor = “通知我去做”
Proactor = “做好了通知我”。
标准/典型的Reactor:
- 步骤1:等待事件到来(Reactor负责)
- 步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)
- 步骤3:读数据(用户处理器负责)
- 步骤4:处理数据(用户处理器负责)
改进实现的模拟Proactor:
- 步骤1:等待事件到来(Proactor负责)
- 步骤2:得到读就绪事件,执行读数据(现在由Proactor负责)
- 步骤3:将读完成事件分发给用户处理器(Proactor负责)
- 步骤4:处理数据(用户处理器负责)
socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIXDomain Socket。
UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息
的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。
UNIX Domain Socket是全双工的,API接口语义丰富,相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制。
使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一
个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构
体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地
址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。
通过accept得到客户端地址也应该是一个socket文件,如果不
是socket文件就返回错误码,如果是socket文件,在建立连接后这个文件就没有用了,调用unlink把它删掉,通过传出参数uidptr返回客户端程序的user id
与网络socket编程不同的是,UNIX Domain Socket客户端一般
要显式调用bind函数,而不依赖系统自动分配的地址。客户端bind一个自己指定的socket文件名
的好处是,该文件名可以包含客户端的pid以便服务器区分不同的客户端。

发送方、接收方依赖IP:Port来标识.
发送方通过系统调用send()将原始数据发送到操作系统内核缓冲区中。内核缓冲区从上到下依次经过TCP层、IP层、链路层的编码,分别添加对应的头部信息,经过网卡将一个数据包发送到网络中。经过网络路由到接收方的网卡。网卡通过系统中断将数据包通知到接收方的操作系统,再沿着发送方编码的反方向进行解码,即依次经过链路层、IP层、TCP层去除头部、检查校验等,最终将原始数据上报到接收方进程。
典型的本地IPC,依赖路径名标识发送方和接收方。即发送数据时,指定接收方绑定的路径名,操作系统根据该路径名可以直接找到对应的接收方,并将原始数据直接拷贝到接收方的内核缓冲区中,并上报给接收方进程进行处理。同样的接收方可以从收到的数据包中获取到发送方的路径名,并通过此路径名向其发送数据。

操作系统提供的接口socket(),bind(),connect(),accept(),send(),recv(),以及用来对其进行多路复用事件检测的select(),poll(),epoll()都是完全相同的。都有tcp和udp的协议。收发数据的过程中,上层应用感知不到底层的差别。