零、前言
本章主要是对套接字网络编程的一个学习,目标是能够基本的进行套接字编程
一、UDP套接字
1、创建套接字
无论是服务端还是客户端,进行网络编程需要做的第一件事就是创建套接字
int socket(int domain, int type, int protocol);
- domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。填写
struct sockaddr 结构的前16位:本地通信设置为AF_UNIX ,网络通信设置为AF_INET (IPv4)或AF_INET6 (IPv6) - type:套接字协议的传输类型:对于UDP的数据报式传输则填入
SOCK_DGRAM ,对于TCP的流式传输则填入SOCK_STREAM - protocol:创建套接字的协议类别。可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,即默认(会根据前两个参数自动推导)
- 返回值:套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
std::cerr<<"socket"<<std::endl;
return 2;
}
std::cout<<"sock:"<<sock<<std::endl;
2、填写ip/port和绑定
对于服务端和客户端都要进行绑定ip及port,只有绑定后才能标识网络中唯一的主机中的进程服务,便于进程接下来的数据传输
sin_family:表示协议家族
sin_port:表示端口号,是一个16位的整数
sin_addr:表示IP地址,是一个32位的整数
sin_addr中的成员s_addr:表示IP地址,是一个32位的整数
- 对于服务端来说,服务端ip和port需要被多个客户端所熟知的,所以服务端的port是需要进行固定化的,也就是说一个服务端的port是该服务端所私有的,不能随意更换
- 对于云服务器上的服务端,不建议绑定明确的ip,建议使用INADDR_ANY绑定该主机所有设备,以此接收向该主机发送的所有数据
- 对于客户端来说,客户端是不提供服务的,ip和port不用被其他主机熟知,并且为了启动客户端的顺利(固定的port被占用会使得进程启动不了),所以不需要我们主动去进行绑定ip和port,当进行数据的发送时,系统会自动绑定ip以及随机的port
- 对于客户端虽然不用主动填写自己的ip和port,但是需要的是明确数据传输的主机中的进程,即需要填写服务端的ip和port
- 对于进行绑定的网络信息字段是需要我们主动进行网络字节序的转化的,系统提供了相应的接口(上面介绍了),而发送的数据系统会在底层进行网络字节序的转化
- 在ip的转化时,我们习惯用的是点分十进制的字符串ip,例如192.168.233.123,但是需要填入的ip形式是四字节整数ip
in_addr_t inet_addr(const char *cp);
- 功能:将点分十进制的字符串IP转换成四字节整数IP
- 传入待转换的字符串IP,该函数返回的就是转换后的整数IP
char *inet_ntoa(struct in_addr in);
- 将四字节整数IP转换成点分十进制字符串IP
- 传入inet_ntoa函数的参数类型是
in_addr ,不需要选中in_addr 结构当中的32位的成员传入,直接传入in_addr 结构体即可
注:上述函数在转化ip格式时同时也会自动进行网络字节序的转化
服务端创建套接字,即底层打开了对应的网络套接字文件,想进行网络通信还需要绑定对应的网络信息,即将套接字文件与网络进行强相关
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等
- addrlen:传入的addr结构体的长度
- 返回值说明:绑定成功返回0,绑定失败返回-1,同时错误码会被设置
- 在绑定时需要将网络相关的属性信息填充到struct sockaddr_in结构体当中,然后将该结构体地址作为bind函数的第二个参数进行传入(这里需要强转为struct sockaddr *addr类型)
- UDP是数据报式套接字,并不会管对端的接收转态,只要绑定后就可以向对端进行接收消息了,但是这样的传输实际中是存有风险的
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[1]));
local.sin_addr.s_addr=htons(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))==-1)
{
std::cout<<"bind"<<std::endl;
return 3;
}
struct sockaddr_in desc;
memset(&desc,sizeof(desc),0);
desc.sin_family=AF_INET;
desc.sin_port=htons(atoi(argv[2]));
desc.sin_addr.s_addr=inet_addr(argv[1]);
3、数据发送和接收
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
- buf:待写入数据的存放位置
- len:期望写入数据的字节数
- flags:写入的方式,一般设置为0,表示阻塞写入
- dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
- addrlen:传入dest_addr结构体的长度
- 返回值:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
注:由于UDP不是面向连接的,所以传输数据时需要指明对端网络相关的信息,即sendto的最后两个参数用来表示对端的信息
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
-
sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据 -
buf:读取数据的存放位位置 -
len:期望读取数据的字节数 -
flags:读取的方式,一般设置为0,表示阻塞读取 -
src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等 -
addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数 -
返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置
注:recvfrom接口的倒数第二个参数是一个输出型参数,用于获取发送消息的对端网络信息,这样就知道是谁发的数据,并可以进一步向对端做出回应
while(1)
{
char buffer[128]={0};
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<"client# "<<buffer<<std::endl;
sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
while(1)
{
std::cout<<"Please Enter# ";
fflush(stdout);
char buffer[128]={0};
ssize_t size=read(0,buffer,sizeof(buffer)-1);
if(size>0)
{
buffer[size-1]=0;
sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<"Echo# "<<buffer<<std::endl;
}
}
}
4、简单回声服务器
当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sento函数将收到的数据重新发送给对应的客户端,以此测试双端的数据的收发功能
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
std::cerr<<"Usage: udpserver port"<<std::endl;
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
std::cerr<<"socket"<<std::endl;
return 2;
}
std::cout<<"sock:"<<sock<<std::endl;
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[1]));
local.sin_addr.s_addr=htons(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))==-1)
{
std::cout<<"bind"<<std::endl;
return 3;
}
while(1)
{
char buffer[128]={0};
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<"client# "<<buffer<<std::endl;
sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
close(sock);
return 0;
}
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
int main(int argc,char* argv[])
{
if(argc!=3)
{
std::cerr<<"Usage: udp_client desc_ip desc_port"<<std::endl;
return 1;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock < 0)
{
std::cerr<<"Usage: udp_client desc_ip desc_port"<<std::endl;
}
struct sockaddr_in desc;
memset(&desc,sizeof(desc),0);
desc.sin_family=AF_INET;
desc.sin_port=htons(atoi(argv[2]));
desc.sin_addr.s_addr=inet_addr(argv[1]);
while(1)
{
std::cout<<"Please Enter# ";
fflush(stdout);
char buffer[128]={0};
ssize_t size=read(0,buffer,sizeof(buffer)-1);
if(size>0)
{
buffer[size-1]=0;
sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)
{
buffer[s]=0;
std::cout<<"Echo# "<<buffer<<std::endl;
}
}
}
close(sock);
return 0;
}
二、TCP套接字
相比于UDP套接字来说,TCP套接字与之在一些地方是相同的,但是TCP的特点是面向链接的流式套接字,所以还是有很大的区别的
1、创建套接字
同样的tcp的服务端和客户端首先第一件事是创建套接字文件
int socket(int domain, int type, int protocol);
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr<<"socket"<<std::endl;
return 2;
}
std::cout<<"sock:"<<sock<<std::endl;
2、填写ip/port和绑定
tcp的服务端和客户端接下来的事也是填写ip/port和绑定
- 对于服务端来说,服务端ip和port需要被多个客户端所熟知的,所以服务端的port是需要进行固定化
- 对于客户端来说,客户端是不提供服务的,不用固定化port,不需要我们主动去进行绑定ip和port,当进行数据的发送时,系统会自动绑定ip以及随机的port
- 对于客户端虽然不用主动填写自己的ip和port,但是需要的是明确数据传输的主机中的进程,即需要填写服务端的ip和port
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr<<"bind"<<endl;
exit(2);
}
struct sockaddr_in desc;
bzero(&desc,sizeof(desc));
desc.sin_family=AF_INET;
desc.sin_port=htons(desc_port);
desc.sin_addr.s_addr=inet_addr(desc_ip.c_str());
3、监听-接收/链接
- 由于TCP是面向链接的套接字,所以需要服务端和客户端建立链接关系
- 对于服务端来说,服务端是会被多个客户端进行链接,由此服务端需要保持等待链接的状态并进行接收链接,等待客户端的链接,这样才能保证链接之后数据传输的可靠性
- 对于客户端来说,客户端是向服务端进行索取服务的一方,即决定链接的发起
int listen(int sockfd, int backlog);
- 功能:设置套接字为监听状态,即服务器时刻注意是否有客户端发来连接请求
- sockfd:需要设置为监听状态的套接字对应的文件描述符
- backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可
- 返回值:监听成功返回0,监听失败返回-1,同时错误码会被设置
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:特定的监听套接字,表示从该监听套接字中获取连接
- addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等
- addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数
- 返回值:获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置
- socket函数创建的套接字文件:用于不断获取客户端发来的连接请求,即进行监听获取链接
- accept函数创建的套接字文件:为本次accept成功获取到的连接提供网络通信服务
if(listen(listen_sock,backlog)<0)
{
cerr<<"listen"<<endl;
exit(3);
}
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
bzero(&peer,len);
int sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sock<0)
{
cout<<"accept error"<<endl;
}
if(connect(sock,(struct sockaddr*)&desc,sizeof(desc)) < 0)
{
cerr<<"connect"<<endl;
}
4、数据发送和接收
- TCP是流式套接字,同文件的读写是一样是流式的,那么对于TCP来说,使用文件读写的方式进行读写套接字文件同样可以达到数据发送和接收的目的
- 读取套接字文件数据,即为接收对应套接字建立链接的远端发送来的消息;向套接字文件进行写入数据,即为向对应套接字建立链接的远端发送数据
ssize_t read(int fd, void *buf, size_t count);
- fd:特定的文件描述符,表示从该文件描述符中读取数据
- buf:数据的存储位置,表示将读取到的数据存储到该位置
- count:数据的个数,表示从该文件描述符中读取数据的字节数
- 返回值:如果大于0,则表示本次实际读取到的字节个数;等于0,则表示对端已经把连接关闭了;小于0,则表示读取时遇到了错误
注:如果客户端将连接关闭了,那么此时服务端将套接字当中的信息读完后就会读取到0,不必再为该客户端提供服务了
ssize_t write(int fd, const void *buf, size_t count);
- fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字
- buf:需要写入的数据
- count:需要写入数据的字节个数
- 返回值:写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
除了使用文件读写函数接口进行发送和接收网络数据,还可以使用专门的数据发送和接收接口
int send(SOCKET s,const char FAR *buf ,int len ,int flags);
- sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中
- buf:待写入数据的存放位置
- len:期望写入数据的字节数
- flags:写入的方式,一般设置为0,表示阻塞写入
- 返回值:入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置
int recv(SOCKET s ,char FAR * buf ,int len ,int flags);
- sockfd:对应操作的套接字文件描述符,表示从该文件描述符索引的文件当中读取数据
- buf:读取数据的存放位位置
- len:期望读取数据的字节数
- flags:读取的方式,一般设置为0,表示阻塞读取
- 返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置
注:因为TCP是面向链接的,每一个读写的套接字文件都已经确立了对应的链接对象,所以这里的recv和send并不用像UDP的recvfrom和sendto那样指定对端的网络信息
- 一般来说 send(),recv()用于TCP,sendto()及recvfrom()用于UDP
- sendto可以在参数中指定发送的目标地址 , sendto可用于无连接的socket,send没有参数指定目标地址,所以需要socket已建立连接
while(true)
{
char buffer[1024]={0};
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"client# "<<buffer<<endl;
string message=buffer;
if(message=="quit")
{
write(sock,"quit success!",strlen("quit success!"));
break;
}
message+="[server_echo]";
write(sock,message.c_str(),message.size());
}
else if(s==0)
{
cout<<"client close..."<<endl;
break;
}
else
{
cerr<<"read error"<<endl;
break;
}
}
while(true)
{
char buffer[1024]={0};
cout<<"Please Enter#";
fflush(stdout);
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s-1]=0;
write(sock,buffer,strlen(buffer));
ssize_t size=read(sock,buffer,sizeof(buffer)-1);
if(size>0)
{
buffer[size]=0;
cout<<buffer<<endl;
}
else
{
cerr<<"server close"<<endl;
break;
}
}
}
5、简单英译汉服务器
#pragma once
#include "server.hpp"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <string.h>
#include <string>
#include <map>
std::map<std::string,std::string> dict={
{"apple","苹果"},
{"banana","香蕉"},
{"hello","你好"},
};
void HandlerTranslation(int sock)
{
cout<<"进行翻译处理: debug sock: "<<sock<<endl;
while(true)
{
char buffer[1024]={0};
ssize_t s=read(sock,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
cout<<"client# "<<buffer<<endl;
string message=buffer;
if(message=="quit")
{
write(sock,"quit success!",strlen("quit success!"));
break;
}
message+="[server_echo]";
write(sock,message.c_str(),message.size());
}
else if(s==0)
{
cout<<"client close..."<<endl;
break;
}
else
{
cerr<<"read error"<<endl;
break;
}
}
}
void* Routine(void* args)
{
int sock=*(int*)args;
delete (int*)args;
pthread_detach(pthread_self());
HandlerTranslation(sock);
close(sock);
return nullptr;
}
void HandlerSock(int sock)
{
pthread_t tid;
int* p=new int(sock);
pthread_create(&tid,nullptr,Routine,p);
}
server.hpp:
#pragma once
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <stdlib.h>
#include <string>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
const int backlog=5;
typedef void(*Handler)(int);
class TcpServer
{
private:
uint16_t _port;
int listen_sock;
public:
TcpServer(uint16_t port)
:_port(port),listen_sock(-1)
{}
void InitTcpServer()
{
listen_sock=socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0)
{
cerr<<"listen_sock"<<endl;
exit(1);
}
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
cerr<<"bind"<<endl;
exit(2);
}
if(listen(listen_sock,backlog)<0)
{
cerr<<"listen"<<endl;
exit(3);
}
}
void Loop(Handler handler)
{
while(true)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
bzero(&peer,len);
int sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sock<0)
{
cout<<"accept error"<<endl;
continue;
}
uint16_t port=ntohs(peer.sin_port);
string ip=inet_ntoa(peer.sin_addr);
cout<<"debug: sock#"<<sock<<" peer_ip#"<<ip<<" peer_port#"<<port<<endl;
handler(sock);
}
}
~TcpServer()
{
if(listen_sock > 0)
close(listen_sock);
}
};
server.cc:
#include "server.hpp"
#include "Handler.hpp"
int main(int argc,char* argv[])
{
if(argc!=2)
{
cout<<"Usage:\n\t"<<"tco_server port"<<endl;
return 1;
}
uint16_t port=atoi(argv[1]);
TcpServer ser(port);
ser.InitTcpServer();
ser.Loop(HandlerSock);
return 0;
}
client.hpp:
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <cstring>
#include <stdio.h>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
class TcpClient
{
private:
string desc_ip;
uint16_t desc_port;
int sock;
public:
TcpClient(string ip,uint16_t port)
:desc_ip(ip),desc_port(port),sock(-1)
{
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
cerr<<"sock"<<endl;
exit(1);
}
}
void start()
{
struct sockaddr_in desc;
bzero(&desc,sizeof(desc));
desc.sin_family=AF_INET;
desc.sin_port=htons(desc_port);
desc.sin_addr.s_addr=inet_addr(desc_ip.c_str());
if(connect(sock,(struct sockaddr*)&desc,sizeof(desc)) < 0)
{
cerr<<"connect"<<endl;
}
while(true)
{
char buffer[1024]={0};
cout<<"Please Enter#";
fflush(stdout);
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s-1]=0;
write(sock,buffer,strlen(buffer));
ssize_t size=read(sock,buffer,sizeof(buffer)-1);
if(size>0)
{
buffer[size]=0;
cout<<buffer<<endl;
}
else
{
cerr<<"server close"<<endl;
break;
}
}
}
}
~TcpClient()
{
if(sock >= 0)
close(sock);
}
};
client.cc:
#include "tcp_client.hpp"
int main(int argc,char* argv[])
{
if(argc!=3)
{
cout<<"Usage:\n\t"<<"tcp_client desc_ip desc_port"<<endl;
return 1;
}
uint16_t port=atoi(argv[2]);
string ip=argv[1];
TcpClient client(ip,port);
client.start();
return 0;
}
- 运行效果:
|