Linux Socket编程
目录
- 前言
- Socket的功能
- Socket基础
- Socket类型
- 基本结构
- 基本转换函数
- 基本Socket使用
- TCP Socket实例
- UDP Socket实例
- 疑难问题记录
- 总结
前言
socket(套接字)是网络编程编程的一种技巧。通过socket不仅可以实现跨进程通信,还可以实现跨主机的网络通信。使用这种技术,就可以实现全国各地的通讯。例如:深圳的一台电脑接收来自北京一台电脑发来的信息。 本篇不涉及太底层的网络原理,仅说明socket的基本使用方法。主要参考《Linux网络编程》。本篇源码获取方式见文底小字。
Socket的功能
socket是通过标准的UNIX文件描述符和其他的程序通讯的一个方法。其可以实现同一主机不同进程间的通信;也可以实现不同主机间的通信。
Socket基础
Socket类型
套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)和原始套接字。
流式套接字(SOCK_STREAM) 流式的套接字可以提供可靠的、面向连接的通讯流。如果你通过流式套接字发送了顺序的数据:"1"、"2"。那么数据到达远程时候的顺序也是"1"、"2"。
面向连接的Socket工作流程
数据报套接字(SOCK_DGRAM) 数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。
原始套接字(SOCK_RAM) 原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
基本结构
- struct sockaddr
此结构用于存储套接字地址。在需要通信地址时,此结构体会被用到,例如
connect()
。
struct sockaddr {
unsigned short sa_family; /* address族, AF_xxx */
char sa_data[14]; /* 14 bytes的协议地址 */
};
sa_family
为指定协议族,通常使用到的有AF_INET、AF_UNIX等。
sa_data
为不同协议族通信时必要的数据。例如,sa_family
为AF_INET时,sa_data
要传IP地址和端口号。
- struct sockaddr_in
sockaddr_in
是用于存储AF_INET的套接字地址,其中in就代表Inet。 介绍sockaddr
时,说到在使用AF_INET需要传IP和端口号,但并不知道要将IP和端口号填到sockaddr
的哪个地方。于是,设计了sockaddr_in
,定义出地址和端口号成员。在使用时只需要填充sockaddr_in
,传参时强转为sockaddr
即可(两个结构体大小一致)。
struct sockaddr_in {
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小) */
};
- struct in_addr
in_addr
为sockaddr_in
成员,用于存储4个字节的IP地址。需要注意的是,此值填写时需要按照网络字节来填充,可以通过一些转换函数完成。
struct in_addr {
unsigned long s_addr;
};
- struct sockaddr_un
sockaddr_un
是用于存储AF_UNIX的套接字地址,推测un就代表UNIX(没有求证)。 和上述类似,当使用AF_UNIX时,需要填充AF_UNIX的地址结构体sockaddr_un
,然后传参时强转为sockaddr
。
struct sockaddr_un
{
uint8_t sun_len;
sa_family_t sun_family; /* AF_LOCAL */
char sun_path[104]; /* null-terminated pathname */
};
基本转换函数
网络字节序转换 上文描述填写IP和端口时要注意网络字节序,否则可能连接不到指定的设备。系统提供了如下几种函数方便转换:
- htons()—— “Host to Network Short” 主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4 bytes)
- htonl()—— “Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8 bytes)
- ntohs()—— “Network to Host Short” 网络字节顺序转换为主机字节顺序(对无符号短型进行操作 4 bytes)
- ntohl()—— “Network to Host Long” 网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8 bytes)
IP地址转换
- inet_addr()—— 把一个用数字和点表示 IP 地址的字符串转换成网络字节序的无符号长整型。
- inet_ntoa()—— “Network to ASCII” 将网络字节序的长整型转换成字符串。
注:inet_ntoa()
返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串。所以每次调用 inet_ntoa(),都会改变最后一次调用 inet_ntoa() 函数时得到的结果。
基本Socket使用
Linux同时支持面向连接和不连接类型的套接字。在面向连接的通讯中服务器和客户机在交换数据之前先要建立一个连接;在不连接通讯中数据被作为信息的一部分被交换。 无论那一种方式,服务器总是最先启动,把自己绑定(Banding)在一个套接字上,然后侦听信息。
socket
主要使用到如下函数:
- socket()函数 —— 创建套接字。
- bind()函数 —— 绑定socket地址信息。(Inet需要传入IP、端口;Unix 需要传入路径)
- connect()函数 —— 连接指定服务器套接字。
- listen()函数 —— 服务器监听连接上的套接字客户端。
- accept()函数 —— 接受远程客户端套接字,会获取到远程连接客户端的地址信息。(阻塞接口)
- send()函数/recv()函数 —— 连接的流式套接字进行通讯的函数。
- sendto()函数/recvfrom()函数 —— 非连接的数据报套接字进行通讯的函数。
- close()函数 —— 关闭套接字描述符所表示的连接。
- shutdown()函数 —— 指定关闭套接字的方式。
- setsockopt()函数/getsockopt()函数 —— 套接字设置项的设置和获取。
- getpeername()函数 —— 取得一个已经连接上的套接字的远程信息。
- getsockname()函数 —— 取得本地主机的信息。
- gethostbyname()函数 —— 通过域名获取IP地址。
- gethostbyaddr()函数 —— 通过一个IPv4的地址来获取主机信息。
- getprotobyname()函数 —— 获得网络协议名。
TCP Socket实例
TCP Socket可以理解为Inet使用流式套接字,为保证通讯稳定而采用TCP协议。其优点在于可靠、稳定。
TCP Socket 服务端
代码语言:javascript复制void DealClientMsgThread(int fd)
{
while(1)
{
char buf[1024] = {0};
int ret = read(fd, buf, sizeof(buf));
if (ret > 0) {
TSVR_LOG("# RECEIVE: %s.n", buf);
// response dstAddr msg
char ack[20] = {0};
snprintf(ack, sizeof(ack), "Ack [%ld]", strlen(buf));
write(fd, ack, strlen(ack));
} else {
break;
}
}
TSVR_LOG("Disconnect.n");
}
int main(int argc, char *argv[])
{
int i = 0;
std::thread threads[MAX_NUM_THREAD];
if (argc != 2)
{
TSVR_LOG("usage ./tcp_server 8080.n");
return -1;
}
string port = argv[1];
int sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd == -1)
{
TSVR_LOGE("socket failed. (%s)n", strerror(errno));
return -1;
}
int op = 1;
if (setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)) < 0) {
TSVR_LOGE("setsockopt failed. (%s)n", strerror(errno));
goto exit;
}
struct sockaddr_in myAddr;
bzero(&myAddr, sizeof(myAddr));
myAddr.sin_family = AF_INET;
myAddr.sin_addr.s_addr = htonl(INADDR_ANY);
myAddr.sin_port = htons(atoi(port.c_str()));
if (bind(sockFd, (struct sockaddr *)&myAddr, sizeof(myAddr)) == -1) {
TSVR_LOGE("bind failed. (%s)", strerror(errno));
goto exit;
}
if (listen(sockFd, MAX_SIZE_BACKLOG) == -1) {
TSVR_LOGE("listen failed. (%s)n", strerror(errno));
goto exit;
}
while(1)
{
// accept
struct sockaddr_in dstAddr;
socklen_t addrSize = (socklen_t)sizeof(dstAddr);
int conFd = accept(sockFd, (struct sockaddr *)&dstAddr, &addrSize);
if (conFd < 0) {
TSVR_LOGE("accept failed. (%s)n", strerror(errno));
continue;
}
if (i < MAX_NUM_THREAD)
{
threads[i] = std::thread(DealClientMsgThread, conFd);
threads[i].detach();
i ;
} else {
TSVR_LOG("The number of threads reaches max(%d).n", MAX_NUM_THREAD);
}
}
exit:
close(sockFd);
return 0;
}
TCP Socket 客户端
代码语言:javascript复制int main(int argc, char *argv[])
{
int op = 1024;
if (argc != 3)
{
TCLT_LOG("usage ./tcp_client <ip> <port>.n");
return -1;
}
string ipAddr = argv[1];
string port = argv[2];
int sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd == -1)
{
TCLT_LOGE("socket failed. (%s)n", strerror(errno));
return -1;
}
if (setsockopt(sockFd, SOL_SOCKET, SO_RCVBUF, &op, sizeof(op)) < 0) {
TCLT_LOGE("setsockopt failed. (%s)n", strerror(errno));
close(sockFd);
return -1;
}
if (setsockopt(sockFd, SOL_SOCKET, SO_SNDBUF, &op, sizeof(op)) < 0) {
TCLT_LOGE("setsockopt failed. (%s)n", strerror(errno));
close(sockFd);
return -1;
}
struct sockaddr_in dstAddr;
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(ipAddr.c_str());
dstAddr.sin_port = htons(atoi(port.c_str()));
// Linux TCP repeat connect directly return EISCONN.
if ( connect(sockFd, (struct sockaddr*)&dstAddr, sizeof(struct sockaddr_in)) < 0
&& errno != EISCONN)
{
TCLT_LOGE("connect %s:%d failed. (%s)n", ipAddr.c_str(), atoi(port.c_str()), strerror(errno));
close(sockFd);
return -1;
}
thread rTh([&]() {
char recvBuf[1024] = {0};
TCLT_LOG("Start write thread.n");
while(1)
{
int ret = read(sockFd, recvBuf, sizeof(recvBuf));
if (ret > 0) {
TCLT_LOG("# RECEIVE: %s.n", recvBuf);
} else {
break;
}
}
});
thread wTh([&]() {
TCLT_LOG("Start read thread.n");
while(1)
{
char sndBuf[] = "Hello World";
int ret = write(sockFd, sndBuf, strlen(sndBuf));
if (ret > 0)
{
TCLT_LOG("# SEND > %s.n", sndBuf);
} else {
break;
}
sleep(2);
}
});
rTh.join();
wTh.join();
return 0;
}
代码是很简单的TCP通讯处理,为方便理解,不做过多封装。
UDP Socket实例
UDP Socket可以理解为Inet使用数据报套接字,为了快速通讯,客户端与服务端约定采用的UDP的套接字通讯。
UDP Socket 服务端
代码语言:javascript复制int main(int argc, char *argv[])
{
struct sockaddr_in dstAddr;
socklen_t addrSize = sizeof(struct sockaddr_in);
if (argc != 2)
{
USVR_LOG("usage ./udp_server 8080.n");
return -1;
}
string port = argv[1];
int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockFd == -1)
{
USVR_LOGE("socket failed. (%s)n", strerror(errno));
return -1;
}
int op = 1;
if (setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)) < 0) {
USVR_LOGE("setsockopt failed. (%s)n", strerror(errno));
goto exit;
}
struct sockaddr_in myAddr;
bzero(&myAddr, sizeof(struct sockaddr_in));
myAddr.sin_family = AF_INET;
myAddr.sin_addr.s_addr = htonl(INADDR_ANY);
myAddr.sin_port = htons(atoi(port.c_str()));
if (bind(sockFd, (struct sockaddr *)&myAddr, sizeof(struct sockaddr_in)) < 0) {
USVR_LOGE("bind failed. (%s)n", strerror(errno));
goto exit;
}
while(1)
{
char buf[1024] = {0};
int ret = recvfrom(sockFd, buf, sizeof(buf), 0,
(struct sockaddr *)&dstAddr, &addrSize);
if (ret > 0) {
char ack[20] = {0};
snprintf(ack, sizeof(ack), "Ack [%ld]", strlen(buf));
USVR_LOG("# RECEIVE: %s.n", buf);
sendto(sockFd, ack, strlen(ack), 0,
(struct sockaddr *)&dstAddr, addrSize);
} else {
USVR_LOG("recvfrom failed. (%s)n", strerror(errno));
break;
}
}
exit:
close(sockFd);
return 0;
}
UDP Socket 客户端
代码语言:javascript复制int main(int argc, char *argv[])
{
if (argc != 3)
{
UCLT_LOG("usage ./udp_client <ip> <port>.n");
return -1;
}
string ipAddr = argv[1];
string port = argv[2];
int sockFd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockFd == -1)
{
UCLT_LOGE("socket failed. (%s)n", strerror(errno));
return -1;
}
int op = 1024;
if (setsockopt(sockFd, SOL_SOCKET, SO_RCVBUF, &op, sizeof(op)) < 0) {
UCLT_LOGE("setsockopt failed. (%s)n", strerror(errno));
close(sockFd);
return -1;
}
struct sockaddr_in dstAddr;
bzero(&dstAddr, sizeof(struct sockaddr_in));
dstAddr.sin_family = AF_INET;
dstAddr.sin_addr.s_addr = inet_addr(ipAddr.c_str());
dstAddr.sin_port = htons(atoi(port.c_str()));
thread wTh([&]() {
UCLT_LOG("Start write thread.n");
while(1)
{
char sndBuf[] = "Hello World";
int ret = sendto(sockFd, sndBuf, strlen(sndBuf), 0,
(struct sockaddr *)&dstAddr, sizeof(struct sockaddr_in));
if (ret > 0) {
UCLT_LOG("# SEND: %s.n", sndBuf);
} else {
break;
}
sleep(2);
}
});
thread rTh([&]() {
struct sockaddr_in dstAddr;
UCLT_LOG("Start read thread.n");
while(1)
{
char recvBuf[1024] = {0};
socklen_t addrSize = sizeof(struct sockaddr_in);
bzero(&dstAddr, addrSize);
int ret = recvfrom(sockFd, recvBuf, sizeof(recvBuf), 0,
(struct sockaddr *)&dstAddr, &addrSize);
if (ret > 0)
{
UCLT_LOG("# RECEIVE: %s.n", recvBuf);
} else {
break;
}
}
});
wTh.join();
rTh.join();
return 0;
}
疑难问题记录
- TCP服务端和客户端如何精确地检测到对方下线或异常断开? ① 接收函数是阻塞的,当对方断开,接收函数会返回异常。 ② 通过错误码和信号判断,当一端异常断开,另一端会收到SIGPIPE信号,再通过getsockopt查询各个套接字确认哪一个断开。
- UDP 如何保证通讯数据稳定 UDP通讯因为不需要连接,从而比TCP通讯更快,但是由于其不确保数据是否安全到达,从而显得不靠谱。为了实现通讯靠谱,需要在应用层建立一套机制验证数据准确性,建立失误重传的约定。
总结
- socket的实现非常优秀,将复杂的网络通信,封装成简单的socket的接口。使用者不用过多考虑TCP、UDP以及其他较底层的网络概念,而快速的实现一套网络通讯的流程。
- 本文仅列举了socket用于inet地址族的例程,其还可以用于UNIX域的进程间通讯。
- 网络编程非常有趣,能够实现天南海北之间的通讯,让远距离的人与人、人与物或者物与物之间产生联系,很有意思!
最后
用心感悟,认真记录,写好每一篇文章,分享每一框干货。