概念
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,自然可以使用文件描述符引用套接字。Linux系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系——“在网络通信中,套接字一定是成对出现的”。一端的发送缓冲区对应另一端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x03e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //unsigned主机IP地址 转 网络IP地址字节序
uint16_t htons(uint16_t hostshort); //unsigned主机端口号 转 网络端口号字节序
uint32_t ntohl(uint32_t netlong); //网络IP地址字节序 转 unsigned主机IP地址
uint16_t ntohs(uint16_t netshort); //网络端口号字节序 转 unsigned主机端口号
IP地址转换函数
要发送出IP地址,需要经过以下转换:
192.168.XX.XX --> unsigned int --> htonl --> 网络字节序
这样比较繁琐,可以用下面的函数一步到位:
192.168.XX.XX --> inet_pton() --> 网络字节序
#include <arpa/inet.h>
/**
* @param src 主机IP地址
* @param dst 网络字节序
*/
int inet_pton(int af, const char *src, void *dst); //主机IP地址 转 网络字节序
/**
* @param src 网络字节序
* @param dst 主机IP地址
* @param size 主机IP地址占用位数
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); //网络字节序 转 主机IP地址
sockaddr数据结构
struct sockaddr诞生于早期,后期出现了替代品sockaddr_in、sockaddr_in6、sockaddr_un等,但由于早期函数设计都使用sockaddr作为参数,难以修改,便沿用了sockaddr当做(void *),所以很多函数在使用时我们传递sockaddr_in或其他,还需再强转成sockaddr。
代码形式:
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
struct sockaddr_in {
sa_family_t sin_family; /* Address family */ 地址结构类型
in_port_t sin_port; /* Port number */ 端口号
struct in_addr sin_addr; /* Internet address */ IP地址
};
struct in_addr { /* Internet address. */
unit32_t s_addr;
};
网络套接字函数
socket模型创建流程图
客户端不用调用bind()是因为不调用会随机分配一个,对于客户端随机一个也无所谓,但如果服务端不调用bind()随机一个,那客户端就无从寻找了。
socket相关函数
socket()
打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol:
传0 表示使用默认协议。
返回值:
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
bind()
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:
socket文件描述符
addr:
构造出IP地址加端口号
addrlen:
sizeof(addr)长度。struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
返回值:
成功返回0,失败返回-1, 设置errno
struct sockaddr *addr赋值:首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
listen()
典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:
socket文件描述符
backlog:
排队建立3次握手队列和刚刚建立3次握手队列的链接数之和
accept()
三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf:
socket文件描述符
addr:
传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen:
传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值:
成功返回一个 新的 socket文件描述符,用于和客户端通信,失败返回-1,设置errno
connect()
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返回-1。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf:
socket文件描述符
addr:
传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen:
传入参数,传入sizeof(addr)大小
返回值:
成功返回0,失败返回-1,设置errno
案例:socket小写转大写服务器
思路
该案例需求为:
服务端工作流程:
- socket() 建立套接字。
- bind() 绑定IP 端口号 (struct sockaddr_in addr 初始化)。
- listen() 指定最大同时发起连接数。
- accept() 阻塞等待客户端发起连接。
- read() 读取客户端发来的字母。
- 字母小写转为大写,使用toupper()函数。
- write() 写入大写字母给客户端。
- close() 关闭服务器。
客户端工作流程:
- socket() 建立套接字。
- bind() 可以不写,“隐式绑定”。
- connect() 向服务器发起连接
- write() 写入小写字母给服务器。
- read() 读取服务器返回的大写字母。
- close() 关闭客户端。
实现
有UI会直观一点,这里就用Android作为开发环境了。
MainActivity:
package com.zyc.networkndk;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_server_start = findViewById(R.id.btn_server_start);
btn_server_start.setOnClickListener(this);
Button btn_server_close = findViewById(R.id.btn_server_close);
btn_server_close.setOnClickListener(this);
Button btn_client_start = findViewById(R.id.btn_client_start);
btn_client_start.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.btn_server_start:
new Thread(new StartServerThread()).start();
break;
case R.id.btn_client_start:
new Thread(new StartClientThread()).start();
break;
case R.id.btn_server_close:
closeServer();
break;
}
}
public native void startServer();
public native void closeServer();
public native void startClient();
class StartServerThread implements Runnable {
@Override
public void run() { startServer(); }
}
class StartClientThread implements Runnable {
@Override
public void run() {
startClient();
}
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_server_start"
android:layout_width="94dp"
android:layout_height="48dp"
android:layout_marginStart="104dp"
android:layout_marginTop="120dp"
android:text="启动服务器"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btn_client_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="104dp"
android:layout_marginTop="64dp"
android:text="启动客户端"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_server_start" />
<Button
android:id="@+id/btn_server_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="120dp"
android:text="关闭服务器"
app:layout_constraintStart_toEndOf="@+id/btn_server_start"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
C++部分用NDK实现。native-lib.cpp:
#include <jni.h>
#include <string>
#include <filesystem>
#include <android/log.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Tag", __VA_ARGS__)
//服务器
int server_lfd = 0;
int server_cfd = 0;
extern "C"
JNIEXPORT void JNICALL
Java_com_zyc_networkndk_MainActivity_startServer(JNIEnv *env, jobject thiz) {
//1.socket()
server_lfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_lfd == -1) {
LOGE("socket server init error");
} else {
LOGE("socket server init ok");
}
//2.为bind准备struct sockaddr
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(11527);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//3.bind()
bind(server_lfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
//4.listen()
listen(server_lfd, 128);
//5.定义struct sockaddr用于接收客户端ip与port信息
struct sockaddr_in clit_addr;
//6.定义socklen_t用于接收客户端ip与port信息结构体
socklen_t clit_addr_len;
clit_addr_len = sizeof(clit_addr);
LOGE("等待连接...");
while (1) {
//7.accept()
server_cfd = accept(server_lfd, (struct sockaddr *) &clit_addr, &clit_addr_len);
if (server_cfd == -1) {
LOGE("socket server accept error");
} else {
LOGE("socket server accept ok",server_cfd);
char clientIp[BUFSIZ];
LOGE("客户端IP:%s , 端口: %d",
inet_ntop(AF_INET, &clit_addr, clientIp, sizeof(clientIp)),
ntohs(clit_addr.sin_port));
char buf[BUFSIZ];
int ret = read(server_cfd, buf, sizeof(buf));
LOGE("服务器收到消息:%s", buf);
//8.转大写处理
for (int i = 0; i < ret; ++i) {
buf[i] = toupper(buf[i]);
}
LOGE("服务器把消息转换成大写:%s", buf);
//9.发消息给客户端
write(server_cfd, buf, sizeof(buf));
}
}
}
//关闭服务器
extern "C"
JNIEXPORT void JNICALL
Java_com_zyc_networkndk_MainActivity_closeServer(JNIEnv *env, jobject thiz) {
close(server_lfd);
close(server_cfd);
}
//客户端
extern "C"
JNIEXPORT void JNICALL
Java_com_zyc_networkndk_MainActivity_startClient(JNIEnv *env, jobject thiz) {
//1.socket()
int client_cfd = 0;
client_cfd = socket(AF_INET, SOCK_STREAM, 0);
if (client_cfd == -1) {
LOGE("socket client init error");
} else {
LOGE("socket client init ok");
}
//2.准备connect()需要用到的 struct sockaddr
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(11527);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
//3.connect()
int ret = connect(client_cfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr));
if (ret != 0) {
LOGE("socket client connect error");
} else {
LOGE("socket client connect ok");
}
//4.发消息给服务器
LOGE("客户端发送消息:hello");
write(client_cfd, "hello", strlen("hello"));
//5.接收服务器消息
char buf[BUFSIZ];
read(client_cfd, buf, sizeof(buf));
LOGE("客户端收到了消息:%s", buf);
LOGE("客户端%d 断开", client_cfd);
close(client_cfd);
}
运行: