Socket本意是(电源)插座,在计算机通信领域中被翻译为“套接字”,是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。通过Socket,两台计算机可以通过网络进行信息的传递。
本篇主要介绍了Socket相关函数的一些基本操作,并给出了客户端和服务器之间通信的例子,其环境是Ubuntu18.04 LTS。
一、Socket相关函数的基本操作
(一)socket()函数
在Linux/Ubuntu环境下,使用socket()函数建立套接字需要引用头文件<sys/socket.h>,其返回值为一个文件描述符(由于一般情况下0、1、2分别表示标准输入、标准输出、标准错误,因此该返回值为在这以后的数字),函数原型如下:
int socket (int __domain, int __type, int __protocol);
1. __domain为协议域,又称协议族,我们最常用的有AF_INET、AF_INET6(也可以写作为PF_INET、PF_INET6),分别代表IPv4地址和IPv6地址。
2. __type为数据传输方式或套接字类型,最常见的有SOCK_STREAM和 SOCK_DGRAM,其中SOCK_STREAM为面向连接的数据传输方式,是基于TCP的协议,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢;而SOCK_DGRAM是无连接的数据传输方式,是基于UDP的协议,即只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。
3. __protocol为传输协议,对应上述的__type,常用的有IPPROTO_TCP 和 IPPTOTO_UDP分别代表TCP和UDP协议。而系统会根据__type的值自行选择,因此该项一般可直接指定为0。
具体使用方法如下:
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
(二)bind()和connect()函数
bind()函数用于服务器端,旨在绑定套接字和自己的IP地址和端口;connect()函数用于客户端,旨在连接套接字和服务器端的IP地址和端口。两个函数的返回值表示是否成功(0表示成功,-1表示错误),函数原型如下:
int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
1. __fd为socket返回的文件描述符。
2. __addr为一个const struct sockaddr *指针,由于兼容性的原因,这里我们只能先使用sockaddr_in 结构体来定义相应的IP地址和端口号,然后再强制转换为 sockaddr 类型的方式。
3. __len为2中结构体的大小,可由sizeof()计算给出。
在使用上述两个函数之前,需要先给sockaddr_in 结构体中的成员赋值,其中sockaddr_in 结构体的成员变量如下:
struct sockaddr_in{
sa_family_t sin_family; //协议族,和socket()函数中一致即可
uint16_t sin_port; //16位的端口号,尽量保证端口号在1024~65536之间
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
具体使用方法如下:
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //先使用0元素对结构体进行初始化
serv_addr.sin_family = AF_INET; //使用IPv4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //定义IP地址
serv_addr.sin_port = htons(1234); //定义端口号
如果是服务器:
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))
客户端:
connect(client_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
(三)listen()和accept()函数
针对服务器端,在绑定了套接字后,还需要使用listen()函数进入到监听状态,然后再调用accept函数进行接收,当接收到来自客户端的请求后就可以建立连接了。
listen()函数返回值表示是否成功(0表示成功,-1表示错误),函数原型如下:
int listen (int __fd, int __n);
1. __fd为socket返回的文件描述符。
2. __n为请求队列的最大长度,也即缓冲区大小。当socket正在处理客户端请求时,如果有新的请求到来,只能被放进缓冲区,这个参数就是表明能接受多少个客户端请求。
accept()函数返回一个新的文件描述符(由于一般情况下0、1、2分别表示标准输入、标准输出、标准错误,因此该返回值为在这以后的数字)表示和对应的客户端进行通信,后续具体的通信都使用这个文件描述符进行数据的传输,函数原型如下:
int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t* __addr_len);
这个函数的参数类似于bind()和connect()的参数,详细的解释大家可以参考上文,只不过这里的sockaddr_in并不需要具体的给出,另外最后的长度参数也是通过指针的方式传进去的。
具体使用方法如下:
listen(serv_sock, 20);
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
int client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_size);
注:listen() 函数只是让socket进入监听的状态,程序会继续执行,直到遇到 accept()函数。但accept() 默认是阻塞模式,会阻塞程序执行(即后面代码不能继续进行),直到有请求的到来。
(四)read()和write()函数
至此,服务器和客户端的连接已经建立好了,就剩调用网络I/O函数进行读写操作了。其中最简单的就属read()和write()函数了,它输入的参数较少,能够适应一般的应用场景,其函数返回值为接受/写入的字节数,函数原型如下:
ssize_t read (int __fd, void *__buf, size_t __nbytes)
ssize_t write (int __fd, const void *__buf, size_t __n)
1. __fd为对应的文件描述符(服务器端为accept建立的,客户端为socket建立的)。
2. __buf为要读取和写入的缓冲区地址,由于数据的传输只能以字符串的形式,因此这里要强制类型转换为(char*)。
3. __n/__nbytes为要读取/写入数据的字节数。
具体使用方法如下:
vector<int> test_array(3);
如果是客户端:
write(client_sock, (char*)test_array.data(), sizeof(int)*test_array.size());
如果是服务器:
read(client_sock, (char*)test_array.data(), sizeof(int)*test_array.size());
(五)close()函数
最后,当完成了数据的传输,就应该关闭相应的socket文件描述符,以备下一次正常的使用。socket提供了close()函数来进行该项操作,其函数返回值表示是否成功(0表示成功,-1表示错误),函数原型如下,输入参数即为相应的文件描述符:
int close (int __fd);
二、Socket通信例程
这里,我们利用上面的知识,给出了服务器订阅客户端发送的vector<int> test_arry = {1, 2, 3}的数据,并显示在终端上的例子。
(一)server.cpp
#include <iostream>
#include <vector>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;
vector<int> test_array(3);
int main(){
//creat socket
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(serv_sock == -1) {
cout << "server_socket failed" << endl;
return -1;
}
//bind the socket to IP and port
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //use 0 to initialize every byte
serv_addr.sin_family = AF_INET; //use IPV4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //define IP
serv_addr.sin_port = htons(1234); //define port
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) { cout << "bind failed" << endl; return -1; }
//enter listening state, wait for the request from client
if (listen(serv_sock, 20) < 0) { cout << "listen failed" << endl; return -1; }
//accept client's request
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
int client_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &client_addr_size);
if(client_sock == -1) { cout << "client_socket failed" << endl; return -1; }
read(client_sock, (char*)test_array.data(), sizeof(int)*test_array.size());
for(int i=0;i<test_array.size();i++){
cout << test_array[i] << " ";
}
cout << endl;
close(client_sock);
close(serv_sock);
return 0;
}
(二)client.cpp
#include <iostream>
#include <vector>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
using namespace std;
vector<int> test_array = {1, 2, 3};
int main(){
//send request to the server with its IP and port
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //use 0 to initialize every byte
serv_addr.sin_family = AF_INET; //use IPV4
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //define IP
serv_addr.sin_port = htons(1234); //define port
//creat socket and establish connection with server
int client_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
connect(client_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
write(client_sock, (char*)test_array.data(), sizeof(int)*test_array.size()); //send data to server
close(client_sock);
return 0;
}
构建完上述两个cpp文件后,对其进行编译生成可执行文件,然后运行即可得到结果。
g++ client.cpp -o client
g++ server.cpp -o server
./server
./client