概念
socket是一种常见的网络编程,使用socket可以实现不同主机间的通信。下面简单理解一下socket编程的大致流程
本篇文章以tcp为例介绍socket的使用
接口介绍
socket();
创建一个套接字,可以使用该套接字进行通信,事实上就是创建了一块缓冲区,OS层面认为它是一个文件,所以返回值是一个文件描述符,通过描述符来操作套接字。
成功返回文件描述符,失败返回-1。
参数:
第一个参数:
使用哪一个代表使用哪种方式,一般使用AF_INET(ipv4)
第二个参数:
常用的有两个:SOCK_STREAM和SOCK_DGRAM,分别是面向字节流和面向数据报,一个供tcp用,一个供udp使用。
第三个参数:协议的配置,一般使用0。
bind();
将本地的ip地址和端口号与刚才创建的socket绑定起来。ip地址在全网中标识唯一一台主机,端口号在一台主机中标识唯一一个进程,所以ip地址加端口号标识了全网内唯一一个进程,将该进程与socket绑定起来,就可以与指定主机的指定进程实现通信。
成功返回0,失败返回-1。
参数:
第一个参数:上一步创建好的socket。
第二个参数:一个结构体,结构体保存要绑定的ip地址和端口号还有要使用的协议,要传入的参数是struct sockaddr* ,但是在使用时不能定义该类型。常使用的有两个:struct sockaddr_in与struct sockaddr_un。ipv4使用struct sockaddr_in,传入时强转为struct sockaddr*。
第三个参数:传入的addr的长度。
listen();
让服务器处于监听状态,因为使用tcp在进行通信前两个主机要先进行链接,确定链接后才能实现通信,监听状态就是让服务器开始等待另一个主机发出链接请求,服务器可以直接相应。
让服务器处于监听状态,便于随时接收链接请求
返回值:成功返回0,失败返回-1。
参数:
第一个参数:要监听的套接字。
第二个参数:请求队列的长度。
请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定,但究竟为多少并没有什么标准,可以根据你的需求来定,并发量小的话可以是10或者20。
如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误
connect();
请求建立链接
返回值:链接成功返回0,失败返回-1
参数:
第一个参数:创建好的套接字
第二个参数:一个结构体,包括使用的协议与要链接的ip地址和端口号,要传入的参数是struct sockaddr* ,但是在使用时不能定义该类型。常使用的有两个:struct sockaddr_in与struct sockaddr_un。ipv4使用struct sockaddr_in,传入时强转为struct sockaddr*。
第三个参数:要传入的addr的长度
accept();
接受链接,与listen不同,listen只是让服务器处于监听状态,可以收到请求,但是与客户端建立链接的是accept函数。
返回值:成功返回一个文件描述符,此后服务器与客户端进行通信时使用的就是该描述符。失败返回-1。
参数:
第一个参数:创建好的套接字。
第二个参数:输出型参数,保存了客户端(请求建立链接的一方)的ip地址等信息。
第三个参数:输出型参数,addr的长度。
send();
发送信息,由于这是对文件描述符进行操作,使用系统调用接口write和read也可以实现发送和接受的任务而不是必须为send和recv。
返回值:实际发送了多少个字符
参数:
第一个参数:创建好的套接字
第二个参数:要发送的字符
第三个参数:期望发送多少个字符
第四个参数:一般使用置0即可。
recv();
接收信息
返回值:实际接收到了多少个字符
参数:
第一个参数:创建好的套接字
第二个参数:存放字符的缓冲区
第三个参数:期望接受多少个字符
第四个参数:一般使用置0即可
客户端与服务器端通信模型
这里添加了一些细节,服务端收到请求连接时创建新的线程处理链接,主线程依旧处于等待链接状态,这样就可以实现多个客户端与服务器同时通信了。
tcpServer.hpp
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <stdlib.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
#include <string.h>
#define BACKLOG 5
class tcpServer{
private:
int _port;
int _sock;
public:
tcpServer(int port):_port(port){}
void init(){
_sock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(_sock < 0){
std::cerr << "socket";
exit(1);
}
sockaddr_in addr;//配置要使用的协议以及IP地址和端口号信息
addr.sin_family = AF_INET;//使用IPv4
addr.sin_addr.s_addr = htonl(INADDR_ANY);//关联本主机所有IP地址
addr.sin_port = htons(_port);//指定端口号
if(bind(_sock, (sockaddr*)&addr, sizeof(addr)) < 0){//绑定套接字
std::cerr << "bind";
exit(2);
}
if(listen(_sock, BACKLOG) < 0){//将服务器设置为监听状态,随时等待客户端链接
std::cerr << "listen";
exit(3);
}
}
~tcpServer(){
close(_sock);
}
static void* run(void* arg){
pthread_detach(pthread_self());//线程分离
char buf[64];
while(true){
ssize_t ret = recv(*(int*)arg, buf, sizeof(buf) - 1, 0);//读取信息
if(ret > 0){
buf[ret] = 0;
if(strcmp(buf, "quit") == 0){//客户端退出,该线程退出
std::cout << "I got quit!" << std::endl;
pthread_exit(0);
}
std::cout << buf << std::endl;
std::string echo = buf;
echo += "(Server echo)";
if(send(*(int*)arg, echo.c_str(), echo.size(), 0) < 0){//向客户端发送回显消息
std::cerr << "send";
exit(4);
}
}
}
}
void start(){
sockaddr_in addr;
socklen_t len;
signal(SIGCHLD, SIG_IGN);//父进程默认处理signal信号
while(true){
int sock = accept(_sock, (sockaddr*)&addr, &len);//接收链接
if(sock < 0){//没有接收到链接,重新接收
continue;
}
else{
std::cout << "get a new link" << std::endl;//收到链接,建立链接,开始通信
pthread_t tid;
pthread_create(&tid, nullptr, run, (void*)&sock);//创建新的线程执行任务
}
}
}
};
tcpServer.cc
#include "tcpServer.hpp"
int main(int argc, char*argv[]){
if(argc != 2){
std::cout << "Please Enter Port!" << std::endl;
exit(1);
}
tcpServer* p = new tcpServer(atoi(argv[1]));//输入端口号
p->init();//服务器初始化
p->start();//服务器启动
delete p;
return 0;
}
tcpClient.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
class tcpClient{
private:
int _sock;
int _port;
std::string _ip;
public:
tcpClient(std::string ip, int port):_port(port),_ip(ip){}//初始化ip和端口号
~tcpClient(){
close(_sock);
}
void init(){
_sock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
if(_sock < 0){
std::cerr << "socket";
exit(1);
}
}
void start(){
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(_ip.c_str());
addr.sin_port = htons(_port);//配置IP地址与端口号和协议
if(connect(_sock, (sockaddr*)&addr, sizeof(addr)) < 0){//建立链接
std::cerr << "connect";
exit(2);
}
char buf[64];
while(true){
ssize_t ret = read(0, buf, sizeof(buf) - 1);//从标准输入读取字符放入缓冲区(cin不可以读取空格)
if(ret > 0){
buf[ret - 1] = 0;
if(strcmp(buf, "quit") == 0){//查看是否为退出标志
break;
}
send(_sock, buf, ret, 0);//不是退出标志,将信息发送过去
ret = recv(_sock, buf, sizeof(buf), 0);//接受服务器发送的返回信息
buf[ret] = 0;
std::cout << buf << std::endl;
}
}
}
};
tcpClient.cc
#include "tcpClient.hpp"
int main(int argc, char*argv[]){
if(argc != 3){
std::cout << "Please enter ip and port!" << std::endl;
exit(1);
}
tcpClient* p = new tcpClient(argv[1], atoi(argv[2]));//传入IP地址与端口号
p->init();
p->start();
delete p;
return 0;
}