socket 通信 实现
了解服务端与客户端如何使用
socket
进行通信,如何绑定socket
?以及如何初始化端口/IP
等信息
什么是socket ?
你可以这样理解它
Socket(套接字)是一种计算机网络通信的工具,可以让不同设备之间进行数据的传输和交流。就像电源插座一样,它允许设备们以一种友好的方式进行交流,只不过他们的语言是计算机语言。所以你可以把Socket看作是互联网世界的通信插座,让设备们能够相互聊天、发送信息,然后不断重复直到世界和平。说白了,Socket就是让计算机之间进行无线“说话”和“传文件”的火爆聚会工具。
在Linux中一切皆文件,socket可以理解成用于网络通信的文件描述符。文件的接收和发送都依靠这一个文件描述符,而通过socket
这个强大的接口与配合绑定端口和ip就完成了一次通信的使命。
在Linux环境中socket
的定义
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
#include <sys/socket.h> // 互联网协议头
extern int socket (int __domain, int __type, int __protocol) __THROW;
参数 -
_domain - 需要使用的是哪种套接字?
AF_UNIX
UNIX 域套接字
AF_UNSPEC
未指定
AF_INET
互联网域套接字
我们使用的比较多的是AF_INET
- _protocol
使用的传输类型:
SOCK_DGRAM
数据报socket
SOCK_STREAM
字节流socket
SOCK_SEQPACKET
排序数据包socket
根据传输协议可以分为UDP和TCP,TCP一般使用SOCK_STREAM ,UDP一般使用SOCK_DGRAM
字节流和数据报的区别:
数据报 是网络传输的数据的基本单元,包含一个报头和数据本身,其中报头描述了数据的目的地以及和其它数据之间的关系。 同一报文的不同分组可以由不同的传输路径通过通信子网。 UDP基于 数据报 。 字节流 方式指的是仅把传输中的报文看作是一个字节序列,在 字节流 服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。
关于字节流和数据报的区别可以参考以下链接:
TCP字节流和UDP数据报区别 - hoohack - 博客园 (cnblogs.com)
用法
int sock_fd = socket(AF_INET, SOCK_STREAM, 0); // 使用ipv4地址
if (sock_fd == -1)
{
perror("socket");
exit(-1);
}
详解sockaddr_in 结构体
#include <netinet/in.h>
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
struct sockaddr_in {
short sin_family; // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
struct in_addr sin_addr; // 4 字节 ,32位IP地址
char sin_zero[8]; // 8 字节 ,不使用
};
struct in_addr {
unsigned long s_addr; // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};
sin_port和sin_addr都必须是网络字节序(NBO),一般可视化的数字都是主机字节序(HBO)。
总结
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
用法
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0,1"); // 使用本地地址
addr.sin_family = AF_INET;
addr.sin_port = htons(1234); // 端口
memset(&addr, 0, sizeof(addr));
socket 通信系列函数
常用的
socket
通信函数
connect 函数
函数作用:
对客户端进行连接
返回值:
成功返回 0,失败返回 -1
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
- 参数
_fd 需要传递的文件描述符
__CONST_SOCKADDR_ARG __addr
// 需要传递强转后的 struct sockaddr_in 结构体,里面包含了ip,端口号 ,地址族等信息
socklen_t __len
// - struct sockaddr_in 结构体的大小
用法
connect(sock_fd, (struct sockaddr *)&addr, sizeof(addr)); // 连接
listen 函数
作用:
将套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接
返回值:
成功返回 0,失败返回 -1
函数定义
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;
参数 -
文件描述符
一个队列
这个队列用于记录正在连接但是还没有连接完成的客户端,一般设置队列的容量为2,3即可。队列的最大容量需要小于30
用法
listen(sock_fd, 20);
accept 函数
作用:
被动监听客户端发起的tcp连接请求,三次握手后连接建立成功。客户端connect
函数请求发起连接。
连接成功后服务器的tcp协议会记录客端的ip和端口,如果是跨网通信,记录ip的就是客户端所在路由器的公网ip
返回值:
成功返回一个可以通信的文件描述符专门用于可以通信成功的客户端,失败返回 -1并设置error
函数定义
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
用法
struct sockaddr client_in; // 接受客户端的请求
socklen_t x = sizeof(client_in);
int clnt_fd = accept(sock_fd, (struct sockaddr *)&client_in, &x);
bind 函数
作用:把用于通信的地址和端口号绑定到socket上
绑定的一定是自己的 ip和和端口,不是对方的;比如对于TCP服务器来说绑定的就是服务器自己的ip和端口
返回值:
成功返回 0,失败返回 -1
函数定义
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
- 参数
_fd 传递的文件描述符
__CONST_SOCKADDR_ARG __addr
强转后的struct sockaddr_in 结构体,包含端口号,ip地址等信息
用法
int ret = bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr)); // 绑定socket
if (ret == -1)
{
perror("bind");
exit(-1); //
}
案例
服务端与客户端进行通信
通信案例 : 服务端接收到客户端的消息打印出来,然后再次把数据发送回去给客户端
服务端 server
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <iostream>
using std::cin;
using std::cout;
int main() {
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
int ret = bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1) {
perror("bind");
exit(-1);
}
listen(sock_fd, 1); // 进入监听状态
struct sockaddr client_in;
socklen_t x = sizeof(client_in);
int clnt_fd = accept(sock_fd, (struct sockaddr *)&client_in, &x);
if (clnt_fd == -1) {
perror("accept");
exit(-1);
}
char buffer[1024];
ret = read(clnt_fd, buffer, sizeof(buffer)); // 从客户端接收字符串
if (ret > 0) {
cout << "Received message from client: " << buffer << std::endl;
}
char s[] = "hello i am server";
write(clnt_fd, s, strlen(s) + 1); // 向客户端发送数据
close(clnt_fd);
close(sock_fd);
return 0;
}
客户端
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <iostream>
using std::cin;
using std::cout;
int main(){
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in serv_addr;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(1234);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
char s[] = "hello i am client ";
write(sock, s, strlen(s) + 1); // 把字符串发送给服务器
char buffer[1024];
read(sock, buffer, sizeof(buffer)); // 从服务器接收字符串
cout << "Received message from server: " << buffer << std::endl;
close(sock);
return 0;
}
多进程版本TCP服务端实现
经过前面的案例和相关函数的介绍,我们也大概明白了网络编程是怎么回事;现在让我们来动手实现一个多进程通信的TCP客户端吧!
步骤分为以下八个流程
- 创建
socket
- 初始化
sockaddr_in
结构体 - 使用
bind
函数将socket
绑定到sockaddr_in
结构体上 listen
监听socket
- 定义用于与客户端通信的
socket
,通过accept
函数返回。但是在此之前,需要先将客户端的sockaddr_in
结构体绑定到accept
上 - 使用
fork
函数创建子进程,父进程继续监听客户端请求 - 从客户端读取数据
- 发送数据到服务端
创建socket
创建服务端的socket,并检查返回值
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
{
perror("socket");
exit(-1);
}
初始化端口,ip地址
初始化sockaddr_in
结构体中的端口,ip地址等信息
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
绑定socket
绑定socket到sockaddr_in 结构体(端口号,ip地址)上
int ret = bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
监听socket
监听socket
中的端口中是否有客户端进来
listen(sock_fd, 1); // 进入监听状态
int clnt_fd;
accept等待三次握手成功后的文件描述符
其中clnt_fd是服务端成功与客户端建立起连接后的文件描述符
struct sockaddr client_in; // 客户端地址
socklen_t x = sizeof(client_in);
clnt_fd = accept(sock_fd, (struct sockaddr *)&client_in, &x); // clnt_fd是客户端的文件描述符
创建子进程
子进程处理请求,父进程继续监听
pid_t pid = fork(); // 多进程为客户端提供服务
if (pid == -1)
{
perror("fork");
exit(-1);
}
if (pid > 0)
{
continue; // 父进程继续监听
}
交互
服务端与客户端进行通信
char buffer[1024];
while (1)
{
ret = read(clnt_fd, buffer, sizeof(buffer)); // 从客户端接收字符串
if (ret > 0)
{
cout << "Received message from client: " << buffer << std::endl;
}
char s[] = "hello i am server";
write(clnt_fd, s, strlen(s) + 1); // 向客户端发送数据
}
四次挥手断开连接
close(clnt_fd);
close(sock_fd);
完整代码
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <iostream>
using std::cin;
using std::cout;
int main()
{
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1)
{
perror("socket");
exit(-1);
}
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
int ret = bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == -1)
{
perror("bind");
exit(-1);
}
listen(sock_fd, 1); // 进入监听状态
int clnt_fd;
while (1)
{
struct sockaddr client_in; // 客户端地址
socklen_t x = sizeof(client_in);
clnt_fd = accept(sock_fd, (struct sockaddr *)&client_in, &x); // clnt_fd是客户端的文件描述符
if (clnt_fd == -1)
{
perror("accept");
exit(-1);
}
pid_t pid = fork(); // 多进程为客户端提供服务
if (pid == -1)
{
perror("fork");
exit(-1);
}
if (pid > 0)
{
continue; // 父进程继续监听
}
char buffer[1024];
while (1)
{
ret = read(clnt_fd, buffer, sizeof(buffer)); // 从客户端接收字符串
if (ret > 0)
{
cout << "Received message from client: " << buffer << std::endl;
}
char s[] = "hello i am server";
write(clnt_fd, s, strlen(s) + 1); // 向客户端发送数据
}
}
close(clnt_fd);
close(sock_fd);
return 0;
}
客户端实现
客户端的实现方式就简单多了,没有服务端那么多流程!让我们看看它有哪几部?首先还是要先创建socket
文件描述符,再初始化IP地址,端口等信息,最后发起连接。此时呢,服务端也就可以跟服务端进行通信了
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <iostream>
using std::cin;
using std::cout;
int main(){
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in serv_addr;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(1234);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
char s[] = "hello i am client ";
write(sock, s, strlen(s) + 1); // 把字符串发送给服务器
char buffer[1024];
read(sock, buffer, sizeof(buffer)); // 从服务器接收字符串
cout << "Received message from server: " << buffer << std::endl;
close(sock);
return 0;
}
面向对象优化
到现在可还是觉得写在主函数里面太过于繁琐,能不能简化一点?让代码的可读性更好!下面我们将代码都封装进了一个类中,并且支持手动输入来进行双向通信!
服务端
#include <cstdio>
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <netinet/in.h>
#include<pthread.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class TCP_Server{
private:
int sock_fd; // 服务端的socket文件描述符
int conn_fd; // accept() 返回的与客户端连接的socket文件描述符
uint16_t port; // 服务端的端口号
std::string ip; // 服务端的ip地址
struct sockaddr_in server_addr;
public:
TCP_Server():sock_fd(-1),conn_fd(-1){
}
bool init_TcP_Server(uint16_t _port ,std::string _ip){
sock_fd = socket(AF_INET,SOCK_STREAM,0);{
perror("socket");
cerr<<"socket error"<<endl; // 发送标准错误信息
}
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(_port);
server_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
if(bind(sock_fd,(struct sockaddr*)&server_addr,sizeof(server_addr))==-1){
perror("bind");
cerr<<"bind error"<<endl;
return false;
}
listen(sock_fd,10);
cout<<"server init success"<<endl; // 初始化成功
return true;
}
void accept_client(){
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
conn_fd = accept(sock_fd,(struct sockaddr*)&client_addr,&len);// 用于通信的socket文件描述符
if(conn_fd==-1){
perror("accept");
cerr<<"accept error"<<endl;
}
cout<<"accept a new client"<<endl;
pid_t pid= fork();
if(pid==-1)
{
perror("fork");
exit(-1);
}
if(pid>0)
{
continue; // 父进程继续监听
}
char buffer[1024]; // 用于存储客户端发送的数据
while(1){
// 从键盘读取数据
int ret = read(conn_fd,buffer,sizeof(buffer));
if(ret>0)
{
cout<<"client send data: "<<buffer<<endl;
}
char s[1024];
// 开始从键盘输入
cin>>s;
write(conn_fd,s,(strlen(s)+1)); // 发送数据
}
}
}
void close(){
::close(sock_fd); // 关闭服务端的socket文件描述符
::close(conn_fd); // 关闭与客户端连接的socket文件描述符
}
};
int main()
{
TCP_Server server;
server.init_TcP_Server(1234,"127.0.0.1"); // 初始化服务端
server.accept_client(); // 接受客户端的连接
server.close(); // 关闭服务端
return 0;
}
客户端
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <iostream>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXLINE 1024
using namespace std;
class TCP_Client{
private:
int sock; // socket descriptor
struct sockaddr_in server; // server's address information
char buffer[MAXLINE]; // buffer for data
uint16_t port; // server's port
string ip; // server's ip
public:
TCP_Client():sock(-1){
}
~TCP_Client(){
close(sock);
}
bool init(string ip,uint16_t port)
{
sock = socket(AF_INET,SOCK_STREAM,0);
if(sock==-1){
perror("socket");
return false;
}
memset(&server,0,sizeof(server));
server.sin_addr.s_addr = inet_addr(ip.c_str());
server.sin_family = AF_INET;
server.sin_port = htons(port);
connect(sock,(struct sockaddr*)&server,sizeof(server));
cout<<"Connected to server"<<endl;
return true;
}
void write_data(){
char s[MAXLINE];
cin.getline(s,MAXLINE); // get input from user
write(sock,s,strlen(s)+1); // send data to server
char buffer[MAXLINE];
read(sock,buffer,MAXLINE); // read data from server
cout<<"Server: "<<buffer<<endl; // print data from server
}
};
int main()
{
TCP_Client client;
client.init("127.0.0.1",1234);
client.write_data();
return 0;
}