[TOC]

套接字作为绝大部分应用的网络编程范式,是非常重要的。本文主要参考了《Linux高性能服务器编程》

一、建立Socket连接

重要的Socket函数介绍如下:

1、创建socket:socket()函数

作用:创建一个Socket文件描述符,唯一标识一个socket

#include<sys/tpyes.h>

#include<sys/socket.h>

int socket(int domain, int type, int protocol);

(1)参数

domain参数指定底层协议族,最常用的是TCP/IP协议族,应该设置为PE_INET(Protocol Family of Internet,用于IPv4)或PE_INET6(用于IPv6);对于UNIX本地域协议族而言,该参数应该设 置为PF_UNIX。

​ type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流 服务)和SOCK_UGRAM(数据报)服务。对TCP/IP协议族而言,其值 取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传 输层使用UDP协议。

​ protocol参数是在前两个参数构成的协议集合下,再选择一个具体 的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下都设置为0,表示使用默认协 议。

(2)返回值

​ 该函数调用成功则返回一个socket文件描述符,失败则返回-1并设置errno。

2、命名socket:bind()函数

作用:把一个地址族中的特定socket地址(IP地址+端口号)赋给socket,通常用于服务端程序。

#include<sys/tpyes.h>

#include<sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

(1)参数

​ sockfd是socket()函数创建的socket文件描述符。

​ addr是struct sockarrr 类型的socket地址指针。

​ addrlen是该socket地址的长度。

(2)返回值

​ 该函数调用成功则返回0,失败则返回-1并设置errno。其中两种常见的 errno是EACCES和EADDRINUSE,它们的含义分别是:

​ EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0~1023)上 时,bind将返回EACCES错误。

​ EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到 一个处于TIME_WAIT状态的socket地址

3、监听socket:listen()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket;

如果作为一个客户端,这时调用connect()发出连接请求,服务器端就会接收到这个请求。

#include<sys/tpyes.h>

#include<sys/socket.h>

int listen(int sockfd, int backlog);

(1)参数

​ sockfd为要监听的socket文件描述符,

​ backlog为内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户 连接,客户端也将收到ECONNREFUSED错误信息

(2)返回值

​ listen成功时返回0,失败则返回-1并设置errno。

4、接收连接:accept()函数

accept只是从监听队列中取出连接而不论连接处于何种状态,更不关心任何网络状况的变化。

#include<sys/tpyes.h>

#include<sys/socket.h>

int accept(int sockfd, struct sockaddr* addr,socklen_t * addrlen);

(1)参数

​ sockfd为要监听的socket文件描述符,

​ addraddr参数用来获取被接受连接的远端socket地址,

​ addrlen参数指出获取的该socket地址的长度。

(2)返回值

​ accept成功时返回一个新的连接socket,失败则返回-1并设置errno。

5、发起连接:connect()函数

connect只是从监听队列中取出连接而不论连接处于何种状态,更不关心任何网络状况的变化。

#include<sys/tpyes.h>

#include<sys/socket.h>

int connect(int sockfd, const struct sockaddr* serv_addr,socklen_t * addrlen);

(1)参数

​ sockfd为要监听的socket文件描述符,

​ addraddr参数用来获取服务端监听的socket地址,

​ addrlen参数指出获取的该socket地址的长度。

(2)返回值

​ connect成功时返回0,失败则返回-1并设置errno。

​ 其中两种常见的errno是ECONNREFUSED和 ETIMEDOUT,它们的含义如下:

​ ECONNREFUSED表示目标端口不存在,连接被拒绝。

​ ETIMEDOUT表示连接超时

6、关闭连接:close()函数和shutdown()函数

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:

#include

int close(int sockfd);

(1)参数

​ sockfd为要关闭的socket文件描述符。

​ close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。 如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的)

#include<sys/socket.h>

int shutdown(int sockfd,int howto);

(2)参数

​ sockfd参数是待关闭的socket。

​ howto参数决定了shutdown的行为。

(3)返回值

​ shutdown成功时返回0,失败则返回-1并设置errno。

二、Socket数据读写

1、TCP数据读写

​ 对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:

#include<sys/tpyes.h>

#include<sys/socket.h>

ssize_t recv(int sockfd,void*buf,size_t len,int flags);

ssize_t send(int sockfd,const void*buf,size_t len,int flags);

​ recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和 大小,flags参数的含义见后文,通常设置为0即可。

​ recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能

要多次调用recv,才能读取到完整的数据。

​ recv可能返回0,这意味着通信对方已经关闭连接了。recv出错时返回-1并设置errno。

​ send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置 errno。

三、Socket的应用

1、服务端程序

#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#include"log/logger.h"

#define MAXLINE 4096
#define DEFAULT_PORT 8000



void Server(int port){
    LOG_INFO("服务端程序启动...\n");
    // 创建一个IPv4的socket地址
    struct sockaddr_in address;//socket地址
    bzero(&address, sizeof(address));//将该地址清空为0
    address.sin_family = AF_INET;//设置IPv4的地址族
    //inet_pton(AF_INET,ip,&address.sin_addr);//手动设置IP地址
    address.sin_addr.s_addr = htonl(INADDR_ANY);// 自动获取本机的IP地址并且设置
    address.sin_port = htons(port);//端口号
    // 创建一个监听套接字
    int m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    if(m_listenfd == -1) {
        LOG_ERROR("创建socket出错:%s(errno:%d)\n",strerror(errno),errno);
    }
    else {
        LOG_DEBUG("创建socket成功\n");
    }
    // 监听套接字绑定socket地址
    int flag = 1;
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));//允许重用本地地址和端口
    int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));//绑定socket地址
    if(ret == -1) {
        LOG_ERROR("绑定socket出错:%s(errno:%d)\n",strerror(errno),errno);
    }
    else {
        LOG_DEBUG("绑定socket成功\n");
    }
    // 开启监听
    ret = listen(m_listenfd, 5);
    if(ret == -1) {
        LOG_ERROR("监听socket出错:%s(errno:%d)\n",strerror(errno),errno);
    }
    else {
        LOG_DEBUG("监听socket成功\n");
    }
    // 开启监听循环
    int connfd=-1;
    char buff[4096];
    int n;
     LOG_INFO("等待客户端请求中...\n");
    while(1){
        // 阻塞等待接受客户端的连接
        struct sockaddr_in client;
        socklen_t client_addrlen=sizeof(client);
        connfd=accept(m_listenfd,(struct sockaddr*)&client,&client_addrlen);
        //connfd=accept(m_listenfd,(struct sockaddr*)NULL,NULL);
        // 如果连接失败
        if(connfd==-1){
            LOG_ERROR("连接socket出错: %s(errno: %d)\n",strerror(errno),errno);
            continue;
        }
        // 如果连接成功
        else{
            // 输出客户端的IP地址和端口号
            char remote[INET_ADDRSTRLEN]; 
            LOG_INFO("连接到客户端,ip:%s and port:%d\n",inet_ntop(AF_INET,&client.sin_addr,remote,INET_ADDRSTRLEN),ntohs(client.sin_port));
        }
        // 从客户端接受数据
        n = recv(connfd,buff,MAXLINE,0);//从connfd中接收最大长度为ΪMAXLINE的数据到buff中,返回接收到的长度
        buff[n] = '\0';
        LOG_INFO("接收到客户端消息: %s\n",buff);
        if(!fork()){
            if(send(connfd,"你好,服务端欢迎你\n",26,0)==-1){
                LOG_ERROR("给客户端发送消息出错: %s(errno: %d)\n",strerror(errno),errno);
            }
            close(connfd);
            exit(0);
        }
        // 关闭客户端
        close(connfd);
    }
    // 关闭socket
    close(m_listenfd);
}



int main(int argc, char * argv[]){
    Server(DEFAULT_PORT);
    return 0;
}

2、客户端程序

可以使用telnet来模拟客户端: telnet 127.0.0.1 8000

#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#include"log/logger.h"//自定义日志模块

#define MAXLINE 4096
#define DEFAULT_PORT 8000
void Client(const char*ip,int port){
    LOG_INFO("客户端程序启动...\n");
     // 创建一个IPv4的socket地址
    struct sockaddr_in server_address; 
    bzero(&server_address,sizeof(server_address)); 
    server_address.sin_family=AF_INET; 
    inet_pton(AF_INET,ip,&server_address.sin_addr); 
    server_address.sin_port=htons(port);
    // 创建一个监听套接字
    int sockfd=socket(PF_INET,SOCK_STREAM,0); 
    if(sockfd == -1) {
        LOG_ERROR("创建socket出错:%s(errno:%d)\n",strerror(errno),errno);
    }
    else {
        LOG_DEBUG("创建socket成功\n");
    }
    // 监听套接字绑定socket地址
    int ret=-1;
    ret=connect(sockfd,(struct sockaddr*)& server_address,sizeof(server_address));
    if(ret == -1) {
        LOG_ERROR("连接服务端出错:%s(errno:%d)\n",strerror(errno),errno);
    }
    else {
        LOG_DEBUG("连接服务端成功\n");
        // 向服务器发送消息
        const char*oob_data="abc"; 
        const char*normal_data="123"; 
        send(sockfd,normal_data,strlen(normal_data),0); 
        send(sockfd,oob_data,strlen(oob_data),MSG_OOB); 
        send(sockfd,normal_data,strlen(normal_data),0);
        // 接收服务器消息
        char buff[4096];
        int n =recv(sockfd,buff,MAXLINE,0); 
        buff[n] = '\0';
        LOG_INFO("接收到服务端消息: %s\n",buff);
    }
    // 关闭socket
    close(sockfd); 
}

int main(int argc, char * argv[]){
    if(argc<=2){
        Client("127.0.0.1",DEFAULT_PORT);
    }
    else{
        const char*ip=argv[1];
        int port=atoi(argv[2]);
        Client(ip,port);
    }
    return 0;
}