以下部分内容参考文章
Socket本质上就是对TCP/IP协议组的封装。是应用层与传输层通信的中间层。从设计模式角度而言,Socket实际上是一组接口,。用户通过这些接口去组织数据,以符合指定的协议。Socket有三种类型,

  • SOCK_STREAM 面向稳定通信,底层是TCP
  • SOCK_DGRAM 无连接的通信,底层是UDP,需要上层协议来保证可靠性。
  • SOCK_RAW 更加灵活的数据控制,可以指定IP头部。

常用Socket编程接口

  1. socket():创建socket
  2. bind():绑定socket到本地地址和端口,通常由服务端调用
  3. listen():TCP专用,开启监听模式
  4. accept():TCP专用,服务器等待客户端连接,一般是阻塞态
  5. connect():TCP专用,客户端主动连接服务器
  6. send():TCP专用,发送数据
  7. recv():TCP专用,接收数据
  8. sendto():UDP专用,发送数据到指定的IP地址和端口
  9. recvfrom():UDP专用,接收数据,返回数据远端的IP地址和端口
  10. closesocket():关闭socket

Socket通信流程

TCP流程

UDP流程

需要注意,TCP和UDP的端口互不干扰,所以说系统可以同时开启TCP80端口和UDP80端口

网络编程模型

  1. 同步阻塞迭代模型
1
2
3
4
5
6
7
8
9
bind(srvfd);
listen(srvfd);
for(;;)
{
clifd = accept(srvfd,...); //开始接受客户端来的连接
read(clifd,buf,...); //从客户端读取数据
dosomthingonbuf(buf);
write(clifd,buf)//发送数据到客户端
}

上述程序弊端:

  • 如果没有客户端的连接请求,进程会阻塞在accept系统调用处,程序不能执行其他任何操作。(系统调用使得程序从用户态陷入内核态)
  • 在与客户端建立好一条链路后,通过read系统调用从客户端接受数据,而客户端发送数据过来是不可控的。如果客户端迟迟不发生数据过来,则程序同样会阻塞在read调用,此时,如果另外的客户端来尝试连接时,都会失败。
  • 同样的道理,write系统调用也会使得程序出现阻塞(例如:客户端接受数据异常缓慢,导致写缓冲区满,数据迟迟发送不出)。
  1. 多进程并发模型
    多进程并发模型在同步阻塞迭代模型的基础上进行了一些改进,以避免是程序阻塞在read系统调用上。核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bind(srvfd);
listen(srvfd);
for(;;){
clifd = accept(srvfd,...); //开始接受客户端来的连接
ret = fork();
switch( ret )
{
case -1 :
do_err_handler();
break;
case 0: // 子进程
client_handler(clifd);
break ;
default : // 父进程
close(clifd);
continue ;
}
}
void client_handler(clifd)
{
read(clifd,buf,...); //从客户端读取数据
dosomthingonbuf(buf);
write(clifd,buf)//发送数据到客户端
}
  1. 多线程并发模型
    在多进程并发模型中,每一个客户端连接开启fork一个进程,若客户端连接较大,则系统依然将不堪负重。通过多线程(或线程池)并发模型,可以在一定程度上改善这一问题。

在服务端的线程模型实现方式一般有三种:

  • 按需生成(来一个连接生成一个线程)
  • 线程池(预先生成很多线程)
  • Leader follower(LF)
    以第一种为例,其核心代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *thread_callback( void *args ) //线程回调函数
{
int clifd = *(int *)args ;
client_handler(clifd);
}

void client_handler(clifd)
{
read(clifd,buf,...); //从客户端读取数据
dosomthingonbuf(buf);
write(clifd,buf)//发送数据到客户端
}
bind(srvfd);
listen(srvfd);
for(;;)
{
clifd = accept();
pthread_create(...,thread_callback,&clifd);
}

在这个模型中,服务端分为了两个线程,一是负责业务逻辑和流的读取。二是accept链接。

  1. IO多路复用
    多进程模型和多线程(线程池)模型每个进程/线程只能处理一路IO,在服务器并发数较高的情况下,过多的进程/线程会使得服务器性能下降。而通过多路IO复用,能使得一个进程同时处理多路IO,提升服务器吞吐量。这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知进程。使得一个进程能在一连串的事件上等待。
    IO复用的实现方式目前主要有select、poll和epoll。select和poll的原理基本相同:
  • 注册待侦听的fd(这里的fd创建时最好使用非阻塞)
  • 每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回
  • 返回结果中包括已就绪和未就绪的fd

相比select,poll解决了单个进程能够打开的文件描述符数量有限制这个问题:select受限于FD_SIZE的限制,如果修改则需要修改这个宏重新编译内核;而poll通过一个pollfd数组向内核传递需要关注的事件,避开了文件描述符数量限制。此外,select和poll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd数量增多而线性增大。epoll的出现,解决了select、poll的缺点:

  • 基于事件驱动的方式,避免了每次都要把所有fd都扫描一遍。

  • epoll_wait只返回就绪的fd。

  • epoll使用nmap内存映射技术避免了内存复制的开销。

  • epoll的fd数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024。

  • select:支持注册 FD_SETSIZE(1024) 个 socket。

  • poll: poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。

  • epoll:epoll 能直接返回具体的准备好的通道,时间复杂度 O(1)。

C在win平台创建socket的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
WSADATA wsaData;
//启动socket
/*
Param1:MAKEWORD 请求的winsock版本
Param2:指向WSADATA的指针
*/
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("PC Server Socket Start Up Error \n");
return -1;
}

int serverSockfd;
/*
创建socket,这里由于我做的是RTSP服务器,所以做了封装,
但基本函数就是 SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
*/
serverSockfd = createTcpSocket();
if (serverSockfd < 0)
{
WSACleanup();
printf("failed to create tcp socket\n");
return -1;
}
//bind
if (bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT) < 0)
{
printf("failed to bind addr\n");
return -1;
}
//listen
if (listen(serverSockfd, 10) < 0)
{
printf("failed to listen\n");
return -1;
}

printf("%s rtsp://127.0.0.1:%d\n",__FILE__, SERVER_PORT);
//循环接收
while (true) {
int clientSockfd;
char clientIp[40];
int clientPort;

clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort);
if (clientSockfd < 0)
{
printf("failed to accept client\n");
return -1;
}

printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);

doClient(clientSockfd, clientIp, clientPort);
}
//注意释放顺序
closesocket(serverSockfd);
WSACleanup();
return 0;

本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

undefined