TCP/UDP的基本Socket编程

套接字是电脑网络中进程间数据流的端点
使用套接字API对UDP/TCP的srver/client进行模拟实现,有助于深刻理解计算机网络

套接字与网络字节序

日常生活中邮件的递送必须需要目标地址
网络信息的递送也需要地址信息,具体来讲,则是:目标IP地址与端口号

可以初步理解为:套接字(Socket)即是 IP address + port( + TCP/UDP )
通过套接字相关函数,才可以完成通信过程。

所谓套接字
是支持TCP/IP的网络通信的基本操作单元,是不同主机之间的进程进行双向通信的端点

实际上套接字是内核中的进程与网络系统的桥梁,进程通过struct socket提取内核网络系统中的数据

IPV4套接字

socket API作为一层抽象的网络编程接口,适用于各种底层网络协议,但不同协议下地址格式并不同
对于IPV4类型的套接字sockaddr_in具体定义如下

1
2
3
4
5
6
7
8
9
10
struct sockaddr_in {
uint8_t sin_len; /* length of structure (16) */
sa_family_t sin_family; /* AF_INET, normally uint8_t */
in_port_t sin_port; /* 16-bit TCP or UDP port number */
/* network byte ordered */
struct in_addr sin_addr; /* 32-bit IPv4 address */
/* network byte ordered */
char sin_zero[8]; /* unused */
// 在使用时,对各个参数赋值,确定套接字的长度、类型、端口、以及其他数据
};

通用类型参数sockaddr

1982年制定套接字地址结构时还没有 void* 的出现,但函数参数需要一种通用的类型
最终,约定使用特殊结构体sockaddr作为通用地址格式
在具体传參时,使用强制转换完成,如

1
2
3
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

BSD实现图示

对于其他常见的套接字类型,举一个较新的BSD实现如下图

其中,sockaddr_storage是一种新的套接字地址结构
相比 sockaddr 存在以下两点差别

  • 如果系统支持的任何套接字地址结构有对齐需要,那么sockaddr_storage能够满足最苛刻的对齐要求
  • sockaddr_storage 足够大,能够容纳系统支持的任何套接字地址结构

网络字节序

数据在内存有大端与小端的区别
由于网络中主机的差异,在数据传输时必须保证大小端的统一
事实上,约定网络数据流以大端的存储方式,在C库中,提供了函数方便大小端数据的相互转换

1
2
3
4
5
6
7
8
9
#include <arpa/inet.h>

uint32_t htonl (uint32_t hostlong);
uint16_t htons (uint16_t hostshort);
uint32_t ntohl (uint32_t netlong);
uint16_t ntohs (uint16_t netshort);

//h表示host,n表示network,l表示32位长整数,s表示16位短整数
//如果是大端,直接返回;如果是小端,转换后返回

TCP与UDP

区别概述

  • TCP面向连接,发送数据之前需要建立连接;UDP面向无连接,开销相对较小
  • TCP保证数据顺序,保证数据交付;UDP尽最大努力交付,开销相对较小
  • TCP面向字节流;UDP是面向报文的,没有拥塞控制,功能少
  • TCP只支持一对一;UDP支持一对一,一对多,多对一和多对多的交互通信
  • TCP的逻辑通信信道是全双工的;UDP半双工

UDP实例

server

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
#include<stdio.h>                                                                      
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main(int argc, char* argv[])
{
if(argc!=3)
{
printf("%s [ip][port]\n",“argument must be);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0); //建立套接字文件描述符
if(sock < 0)
{
perror("socket");
return 2;
}

struct sockaddr_in local;
local.sin_family = AF_INET; //IPV4协议
local.sin_port = htons(atoi(argv[2])); //先转数字再转大端
local.sin_addr.s_addr = inet_addr(argv[1]); //点分十进制转4字节类型
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0) //绑定套接字与端口
{
perror("bind");
exit(1);
}

char buf[1024];
struct sockaddr_in client;
socklen_t len=sizeof(client);
while(1)
{
ssize_t _s=recvfrom(sock, buf, sizeof(buf)-1, \
0, (struct sockaddr*)&client, &len); //接收数据
if(_s>0)
{
buf[_s]='\0';
printf("client:[ip:%s][port:%d] \
%s", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buf);

else
{
break;
}

sendto(sock, buf, sizeof(buf), \
0, (struct sockaddr*)&client, &sizeof(client); //发送数据
}
close(sock);
return 0;
}

其中

  • int socket(int domain, int type, int protocol);
    • 建立套接字文件描述符,应用程序可以像读写文件一样收发数据
    • domain:用于设置网络通信的域,函数该参数选择通信协议的族,AF_INET表示IPV4
    • type:用于设置套接字通信的类型
      • SOCKET_STREAM(流式套接字,TCP)
      • SOCK_DGRAM(数据包套接字,UDP)
    • protocol:一般为0
    • 成功返回一个标识这个套接字的文件描述符,失败返回-1
  • int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
    • 对套接字进行地址和端口的绑定后才能进行数据的收发
    • sockfd:是用socket()函数创建的文件描述符
    • my_addr:是指向套接字地址格式结构体的指针
    • addrlen:是my_addr结构的长度,可以为sizeof(struct sockaddr)
    • 成功返回0,失败返回-1
  • IPV4地址转换函数(点分十进制与4字节格式互转)
    • 点分十进制转4字节int inet_aton(const char *string, struct in_addr*addr);
      • 成功返回非0整数
    • 4字节转点分十进制char *inet_ntoa (struct in_addr);
      • 失败返回NULL
      • 返回值位于静态区,下一次调用会覆盖上一次的结果,可能影响线程安全
  • 更安全强大的地址转换函数(但比aton、ntoa参数多)
    • const char *inet_pton(int domain, const void *restrict addr, char *restrict str, socklen_t size);
      • 失败返回NULL
    • int inet_pton(int domain, const char *restrict str, void *restrict addr);
      • 成功返回1,格式无效返回0,出错返回-1

client

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
// 头文件略
int main(int argc,char* argv[])
{
if(argc != 3){
printf("%s [ip][port]\n",“argument must be);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
int _port = atoi(argv[2]);
char* _ip = argv[1];
struct sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(_port);
client.sin_addr.s_addr = inet_addr(_ip);
char buf[1024];
while(1)
{
ssize_t _s=read(0, buf, sizeof(buf)-1);
if(_s>0)
buf[_s]='\0';

_s=sendto(sock, buf, sizeof(buf)-1, 0, \
(struct sockaddr*)&client,sizeof(client));
}
return 0;
}

TCP实例

server

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>

void ProcessRequest(int client_fd, struct sockaddr_in* client_addr)
{
char buf[1024] = {0};
for (;;)
{
ssize_t read_size = read(client_fd, buf, sizeof(buf));
if (read_size < 0)
{
perror("read");
continue;
}
if (read_size == 0)
{
printf("client: %s say bye!\n", inet_ntoa(client_addr->sin_addr));
close(client_fd);
break;
}
buf[read_size] = '\0';
printf("client: %s say: %s\n", inet_ntoa(client_addr->sin_addr), buf);
write(client_fd, buf, strlen(buf));
}
return ;
}

void* CreateWorker(void* ptr)
{
Arg* arg = (Arg*)ptr;
ProcessRequest(arg->fd, &arg->addr);
free(arg);
return NULL;
}

typedef struct Arg
{
int fd;
struct sockaddr_in } Arg;
buf, strlen(buf));
addr;
}

int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("%s [ip][port]\n",“argument must be);
return 1;
}

//建立套接字文件描述符并绑定、监听端口
struct sockaddr_in addr;
addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("socket");
return 1;
}
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return 1;
}

ret = listen(fd, 10);
if (ret < 0)
{
perror("listen");
return 1;
}

//多线程不断服务新的请求
for (;;)
{
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);
if (client_fd < 0)
{
perror("accept");
continue;
}

//文件描述符正常,准备创建线程
pthread_t tid = 0;
Arg* arg = (Arg*)malloc(sizeof(Arg));
arg->fd = client_fd;
arg->addr = client_addr;

//创建并分离线程
pthread_create(&tid, NULL, CreateWorker, (void*)arg);
pthread_detach(tid);
}
return 0;
}

其中

  • int listen(int sockfd, int backlog);
    • 将sockfd指定为接收连接请求的套接字
    • connect请求发生时,完成三次握手后会将连接放到制定队列中,直到被accept处理
    • 如果这个队列满了,且有新的连接的时候,对方会收到出错信息
    • sockfd:指定的套接字描述符
    • backlog:等待连接的队列大小
    • 成功返回0,失败返回-1
  • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    • sockfd:socket函数返回的套接字描述符
    • addr和addrlen:用来返回已连接的对端进程(客户端)的协议地址(输出型参数)
    • 服务器调用accept后客户端没有数据时,服务器进入阻塞
    • 如果对客户端的协议地址不感兴趣,可以把arrd和addrlen置为空指针
    • 成功返回非负描述符,失败返回-1

client

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
60
61
62
#include <stdio.h>                                                                                                                                      
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

int main(int argc,char* argv[])
{
if(argc !=3)
{
printf("%s [ip][port]\n",“argument must be);
return 1;
}

int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("scok");
return 3;
}

struct sockaddr_in server_sock;
server_sock.sin_family = AF_INET;
server_sock.sin_port = htons(atoi(argv[2]));
server_sock.sin_addr.s_addr = inet_addr(argv[1]);

int ret = connect(sock,(struct sockaddr*)&server_sock,sizeof(server_sock));
if(ret < 0)
{
perror("connect");
close(sock);
return 2;
}

printf("connect success...\n");
char buf[1024];
while(1)
{
printf("please enter :");
fflush(stdout);
ssize_t _s = read(0,buf,sizeof(buf));
if(_s > 0)
buf[_s] ='\0';
else if(_s == 0)
{
close(sock);
break;
}
else
{
break;
}

write(sock, buf, strlen(buf));
printf("please wait...\n");
read(sock, buf, sizeof(buf));
printf("server # :%s",buf);
}
return 0;
}

其中

  • int connect (int sockfd ,struct sockaddr *serv_addr, int addrlen);
    • 用于客户端建立tcp连接
    • sockfd:套接字文件描述符
    • serv_addr:目标主机地址和端口号
    • addrlen:缓冲区的长度
    • connect会激发TCP的三路握手过程
    • 成功返回0,失败返回-1