【Linux | 网络编程】01 套接字概念与TCP/UDP通信基础操作
1 网络编程
1.1 套接字概念
socket在编程中是网络通信的端点。在Linux中,socket被实现为一种特殊类型的“文件”(伪文件),通过文件描述符操作。读写socket和读写文件几乎是一样的接口(如read()、write()、close())。
socket并不是协议,它只是对TCP/IP协议族的一种封装。通信本质是通过协议进行的,socket只是编程接口(API)。不同语言调用的socket接口可能略有差别,但底层逻辑相同。
一个socket由(IP地址, 端口号)唯一标识。两个进程之间的连接由两个socket组成:(源IP:源端口,目标IP:目标端口) ,称为socket pair。socket pair唯一标识一条TCP连接(或UDP会话)。在网络通信中,套接字一定是成对出现的。 一端的发送缓冲区对应对端的接收缓冲区。

1.2 地址信息设置
以IPv4为例介绍网络的地址结构。主要涉及的结构体有struct in_addr、structsockaddr、struct sockaddr_in。
struct sockaddr是一种通用的地址结构,它可以描述一个IPv4或者IPv6的结构,所有涉及到地址的接口都使用了该类型的参数,但是过于通用的结果是直接用它来描述一个具体的IP地址和端口号十分困难。
struct sockaddr {
sa_family_t sin_family; // 地址协议,如果为IPv4,则指定为AF_INET
char sa_data[14]; // 保留字节,并没有指定
};
所以用户一般先使用struct sockaddr_in来构造地址,再将其进行强制类型转换成struct sockaddr以作为网络接口的参数。
struct sockaddr_in {
sa_family_t sin_family; // 地址协议,如果为IPv4,则指定为AF_INET
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IP地址,点分十进制
};
struct in_addr {
uint32_t s_addr; // 4字节的整型数据
};
1.3 大小端地址转换
对于一个存储空间大于1个字节的数据,在内存中有两种存储模式:
-
大端模式(big-endian):数据的高字节在内存的低地址存放,数据的低字节在内存的高地址存放;
大端模式又称为网络字节序;

-
小端模式(little-endian):数据的高字节在内存的高地址存放,数据的低字节在内存的低地址存放;
小端字节序又称为主机字节序。

TCP/IP协议规定,当数据在网络中传输的时候,一律使用网络字节序即大端法。对于应用层协议的载荷部分,如果不需要第三方工具检测内容,可以不进行大小端转换(因为接收方和发送方都是主机字节序即小端法)。但是对于其他层次的头部部分,在发送之前就一定要进行小端到大端的转换了(因为网络中的通信以及tcpdump 、netstat等命令都是以大端法来解析内容的)。
整数大小端转换相关的函数有:
#include <arpa/inet.h>
//h --> host, n --> net, l --> 32bit, s --> 16bit
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
1.4 IP地址的转换
点分十进制的IP地址转换为4位网络字节序IP地址的函数为:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp); // inp为传出参数
in_addr_t inet_addr(const char *cp);
4位网络字节序IP地址转换为点分十进制的IP地址的函数为:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af,
const void *src, // 网络字节序的IP地址
char *dst, // 可视化的IP地址,传出参数
socklen_t size); // 数组长度
1.5 域名和IP地址的对应关系
DNS协议建立了域名和IP地址的映射关系,去访问一个DNS服务器,服务器当中存储了域名和IP地址的映射关系。与这个操作相关的函数为:
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 主机的官方域名(规范名)
char **h_aliases; // 主机的别名列表(以NULL结尾)
int h_addrtype; // 地址类型(AF_INET表示IPv4,AF_INET6表示IPv6)
int h_length; // 地址长度(IPv4是4字节,IPv6是16字节)
char **h_addr_list; // IP 地址列表(以NULL结尾,存储的是网络字节序的二进制地址)
}
2 TCP通信
2.1 TCP通信的流程
使用TCP通信的流程图:

2.2 相关接口
2.2.1 socket
socket函数用于创建一个socket设备。调用该函数时需要指定通信的协议域、套接字类型和协议类型。一般根据选择TCP或者UDP有着固定的写法。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain AF_INET --> IPv4, AF_INET6 --> IPv6
//type SOCK_STREAM --> TCP, SOCK_DGRAM --> UDP
//protocol IPPROTO_TCP --> TCP, IPPROTO_UDP -->UDP
// 返回值 成功 --> 大于0(一个文件描述符), 失败 --> -1(没有得到套接字)
实际使用时参数3一般直接指定为0。
2.2.2 bind
bind函数用于给套接字赋予一个本地协议地址(即IP地址加端口号)。bind在选择端口号时应当避开知名端口号的范围(<1024)。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd 指定一个套接字
// addr 通用的sockaddr,实际绑定时,会使用struct sockaddr_in
// addrlen 其值就是sizeof(struct sockaddr_in)
// 返回值 成功 --> 0, 失败 --> -1(设置errno)
该函数只有服务器端需要进行调用,因为服务器需要将自己的网络地址暴露给客户端。
客户端不需要使用bind,操作系统会自动进行分配一个网络地址。
2.2.3 listen
listen函数使TCP服务端开启监听。服务端在开启了listen之后,就可以开始接受客户端连接了。
int listen(int sockfd, int backlog);
// sockfd 监听某一个套接字
// backlog 监听的连接的个数
// 返回值 成功 --> 0, 失败 --> -1(设置errno)
一旦启用了listen之后,操作系统就知道该套接字是服务端的套接字,操作系统内核就不再启用其发送和接收缓冲区,转而在内核区维护两个队列结构:半连接队列和全连接队列。半连接队列用于管理成功第一次握手的连接,全连接队列用于管理已经完成三次握手的队列。backlog在有些操作系统用来指明半连接队列和全连接队列的长度之和,一般填一个正数即可。如果队列已经满了,那么服务端受到任何再发起的连接都会直接丢弃(大部分操作系统中服务端不会回复RST,以方便客户端自动重传)。

使用netstat -an命令可以查看主机上某个端口的监听情况:
$ netstat -an|grep 2778
tcp 0 0 127.0.0.1:2778 0.0.0.0:* LISTEN
2.2.4 connect
客户端使用connect来建立和TCP服务端的连接。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd 客户端的套接字
// addr 指定服务器的网络地址
// addrlen 网络地址的长度
// 返回值 成功 --> 0, 失败 --> -1(设置errno)
客户端在调用connect可以不使用bind来指定本地的端口信息,这时客户端就会随机选择一个临时端口号来作为源端口。调用connect预期是完成TCP建立连接的三次握手。如果服务端未开启对应的端口号或者未监听,则只能收到一个RST回复,并且报错返回的内容是"Connection refused"。
2.2.5 accept
accept函数由服务端调用,用于从全连接队列中取出下一个已经完成的TCP连接。如果全连接队列为空,那么accept会陷入阻塞。一旦全连接队列中到来新的连接,此时accept操作就会就绪,这种就绪是读操作就绪,所以可以使用select函数的读集合进行监听。当accept执行完了之后,内核会创建一个新的套接字文件对象,该文件对象关联的文件描述符是accept的返回值,文件对象当中最重要的结构是一个发送缓冲区和接收缓冲区,可以用于服务端通过TCP连接发送和接收TCP段。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// sockfd 指定监听的套接字listenfd
// addr 传入传出参数,存放客户端的网络地址
// addrlen addr长度信息 ,传入传出参数
// 返回值 成功 --> 大于0(一个套接字peerfd), 失败 --> -1(设置errno)
默认情况下,accept是一个阻塞式的函数。
两类套接字:
- 通常把旧的管理连接队列的套接字称作监听套接字listenfd。监听套接字会一直存在,负责建立各个不同的TCP连接(只要源IP、源端口、目的IP、目的端口四元组任意一个字段有区别,就是一个新的TCP连接),执行三次握手。
- 新的用于发送和接收TCP段的套接字称作已连接套接字peerfd。某一条单独的TCP连接则是由其对应的已连接套接字进行数据通信的。
2.2.6 send和recv
send和recv用于将数据在用户态空间和内核态的缓冲区之间进行传输,无论是客户端还是服务端均可使用,但是只能用于TCP连接。将数据拷贝到内核态并不意味着会马上传输,而是会根据时机再由内核协议栈按照协议的规范进行分节,通常缓冲区如果数据过多会分节成MSS的大小,然后根据窗口条件传输到网络层之中。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// sockfd server 用peerfd来接收数据 client:用clientfd来接收数据
// buf 指定的是用户态的发送缓冲区的首地址
// len 指定的是用户态的发送缓冲区的长度
// flags 一般情况下指定为0
// 返回值 成功 --> 大于0(发送的字节数), 失败 --> -1(设置errno)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// sockfd server:用peerfd来接收数据 client:用clientfd来接收数据
// buf 指定的是用户态的接收缓冲区的首地址
// len 指定的是用户态的接收缓冲区的长度
// flags 一般情况下指定为0,还可设置为MSG_PEEK,MSG_WAITALL,MSG_DONTWAIT
// 返回值 大于0 --> 接收的数据长度
// 等于0 --> 连接断开
// 小于0 --> 发生了错误,设置errno
使用read和write可以实现同样的效果,相当于flags参数为0。
需要特别注意的是, send和recv的次数和网络上传输的TCP段的数量没有关系,多次的send和recv可能只需要一次TCP段的传输。另外一方面,TCP是一种流式的通信协议,消息是以字节流的方式在信道中传输,这就意味着,消息和消息之间是没有边界的。在不加额外约定的情况下,通信双方并不知道发送和接收到底有没有接收完一个消息,有可能多个消息会在一次传输中被发送和接收(粘包),也有有可能一个消息需要多个传输才能被完整的发送和接收(半包)。
send和recv都是阻塞式函数:
- 对于
recv函数来说,当内核态接收缓冲区没有数据时,就处于阻塞状态; - 对于
send函数来说,当内核态的发送缓冲区满了就会阻塞。
2.3 实现服务端-客户端通信
2.3.1 代码实现
server服务端:
#include <func.h>
int main(int argc, char *argv[])
{
signal(SIGPIPE, SIG_IGN);
ARGC_CHECK(argc, 3);
int ret = 0;
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(listenfd, -1, "socket");
printf("listenfd : %d.\n", listenfd);
int opt = 1;
ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
ERROR_CHECK(ret, -1, "setsockopt");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(listenfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
ERROR_CHECK(ret, -1, "bind");
ret = listen(listenfd, 10);
ERROR_CHECK(ret, -1, "listen");
printf("server start listening!\n");
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(struct sockaddr);
int peerfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
printf("%s:%d has connected, peerfd:%d.\n",
inet_ntoa(clientaddr.sin_addr),
ntohs(clientaddr.sin_port),
peerfd);
char sendbuff[128] = "hello, client!";
ret = send(peerfd, sendbuff, strlen(sendbuff), 0);
printf("send %d bytes.\n", ret);
char recvbuff[128];
recv(peerfd, recvbuff, sizeof(recvbuff), 0);
printf("recv msg: %s\n", recvbuff);
close(peerfd);
close(listenfd);
return 0;
}
client服务端:
#include <func.h>
int main(int argc, char *argv[])
{
ARGC_CHECK(argc, 3);
int ret = 0;
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(clientfd, -1, "socket");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(clientfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
ERROR_CHECK(ret, -1, "connect");
printf("connection success!\n");
char recvbuff[128] = {0};
recv(clientfd, recvbuff, sizeof(recvbuff), 0);
printf("recv msg: %s\n", recvbuff);
char sendbuff[128] = "hello, server!";
ret = send(clientfd, sendbuff, strlen(sendbuff), 0);
printf("send %d bytes.\n", ret);
return 0;
}
2.3.2 SIGPIPE信号处理
当往一个已经断开了的连接上发送数据,第一次send时,由于客户端已经断开,内核会给服务器返回一个RST报文。当第二次send时,服务器进程会收到一个SIGPIPE信号,其默认处理方式是终止进程,直接导致进程奔溃。因此,需要在服务器程序的开始位置,直接将SIGPIPE信号忽略掉。
// 在 main 函数开始处调用
signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE 信号
2.3.3 套接字属性设置
当服务器主动断开链接时,会进入到TIME_WAIT状态,此时想要再次启动服务器进程,会报以下的错误bind: Address already in use。为此,必须对套接字的属性进行设置。
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// sockfd server要设置选项的套接字描述符
// level 选项所属的协议层:
// SOL_SOCKET:通用套接字层(最常用,如端口复用)
// IPPROTO_TCP:TCP协议层(如TCP保活)
// IPPROTO_IP:IP协议层(如设置TTL)
// optname 具体要设置的选项名(如 SO_REUSEADDR、SO_SNDBUF)
// optval 指向存放选项值的缓冲区(传入要设置的值)
// optlen optval缓冲区的大小(字节数,用 sizeof() 获取)
// 返回值 成功 --> 大于0(一个套接字peerfd), 失败 --> -1(设置errno)
在bind前,将listenfd的套接字的属性设置为网络地址可以重用的:
int opt = 1;
ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2.4 实现http服务器
#include <asm-generic/socket.h>
#include <func.h>
int main(int argc, char *argv[])
{
ARGC_CHECK(argc, 3);
int ret;
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(listenfd, -1, "socket");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));//初始化
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
int on = 1;
ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
ERROR_CHECK(ret, -1, "setsockopt");
ret = bind(listenfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
ERROR_CHECK(ret, -1, "bind");
ret = listen(listenfd, 10);
ERROR_CHECK(ret, -1, "listen");
while(1)
{
struct sockaddr_in clientAddr;
memset(&clientAddr, 0, sizeof(clientAddr));
socklen_t len = sizeof(clientAddr);
int peerfd = accept(listenfd, (struct sockaddr*)&clientAddr, &len);
printf("%s:%d has connected.\n",
inet_ntoa(clientAddr.sin_addr),
ntohs(clientAddr.sin_port));
//1.获取http请求报文
char buff[4096] = {0};
recv(peerfd, buff, sizeof(buff), 0);
//2.对请求报文进行响应操作
const char * startLine = "HTTP/1.1 200 OK\r\n";
const char * headers = "Server: MyHttpServer1.0\r\n"
"Content-Type: text/html\r\n"
"Content-Length: ";
const char * emptyLine = "\r\n";
const char * body =
"<html><head>This is a test!</head>\n"
"<body><br>Hello,client!</body></html>";
//清空缓冲区
memset(buff, 0, sizeof(buff));
sprintf(buff, "%s%s%ld\r\n%s%s",
startLine,headers,strlen(body), emptyLine,body);
printf("response:\r\n%s\n", buff);
printf("-------------------------end---------------------------\n\n\n");
send(peerfd, buff, strlen(buff), 0);
}
close(listenfd);
return 0;
}
3 UDP通信
3.1 UDP通信的流程
使用UDP通信的流程图:

UDP是一个无连接的协议,因此必须要客户端先发送数据,这样服务端才能知道对端的地址信息,从进入后续的通信。
3.2 sendto和recvfrom
sendto和recvfrom这两个函数的行为类似于send和recv,不过会有一些额外的参数,用来指定或者保存对端的套接字地址结构以及对应的大小。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sendto发送数据时,必须要指定网络地址。recvfrom表示从某一个网络地址接收数据。
3.3 实现服务端-客户端通信
server服务端:
#include <func.h>
int main(int argc, char *argv[])
{
ARGC_CHECK(argc, 3);
int ret = 0;
int serverfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(serverfd, -1, "socket");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
ret = bind(serverfd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
ERROR_CHECK(ret, -1, "bind");
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t len = sizeof(struct sockaddr);
char recvbuff[128] = {0};
ret = recvfrom(serverfd, recvbuff, sizeof(recvbuff), 0, (struct sockaddr*)&clientaddr, &len);
printf("recvfrom client: %s, bytes: %d.\n", recvbuff, ret);
printf("client: %s:%d.\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
char sendbuff[] = "hello, client!";
ret = sendto(serverfd, sendbuff, strlen(sendbuff), 0, (const struct sockaddr*)&clientaddr, sizeof(clientaddr));
printf("send %d bytes.\n", ret);
close(serverfd);
return 0;
}
client服务端:
#include <func.h>
int main(int argc, char *argv[])
{
ARGC_CHECK(argc, 3);
int ret = 0;
int clientfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(clientfd, -1, "socket");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(serveraddr);
char sendbuff[128] = "hello, server!";
ret = sendto(clientfd, sendbuff, strlen(sendbuff), 0, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
printf("send %d bytes.\n", ret);
char recvbuff[128] = {0};
recvfrom(clientfd, recvbuff, sizeof(recvbuff), 0, (struct sockaddr*)&serveraddr, &len);
printf("recv msg: %s\n", recvbuff);
// memset(recvbuff, 0, sizeof(recvbuff));
// recvfrom(clientfd, recvbuff, sizeof(recvbuff), 0, (struct sockaddr*)&serveraddr, &len);
// printf("recv msg: %s\n", recvbuff);
close(clientfd);
return 0;
}
UDP是一个传输层的协议,即在传输层无法解决数据可靠传输的特性。要达到数据可靠传输的效果,只能交给上层(应用层)来处理,因此UDP的通信要比TCP难得多,应用层必须要将TCP的功能实现一次。
更多推荐



所有评论(0)