TCP/IP网络编程(1)——基于TCP的服务端和客户端的简单实现

目录

前言

一、服务器端函数

1. 创建套接字函数 socket

2. 套接字绑定地址函数 bind

3. 等待连接请求函数 listen

4. 处理连接请求函数 accept

5. 关闭套接字函数 close

二、客户端函数

1. 请求连接函数 connect

三、完整代码

四、 基于TCP的半关闭 shutdown

五、 套接字可选项 getsockopt & setsockopt

 


前言

本系列是阅读尹圣雨所著TCP/IP网络编程一书的学习笔记,我将记录一些关键知识和遇到的问题,在最后能够自己搭建一个简易的服务器。

本文主要介绍TCP服务端和客户端的一些关键函数


一、服务器端函数

1. 创建套接字函数 socket

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

调用成功则返回文件描述符,失败则返回-1。为了方便程序员指定套接字,套接字创建时操作系统会自动给套接字分配一个整数,这个整数就是文件描述符。0、1、2是标准输入输出的文件描述符。

(1)domain: 套接字中使用的协议族。常用的有PF_INET(ipv4协议族)、PF_INET6(ipv6协议族)。协议族的类型将决定protocol参数类型的范围。

(2)type: 数据传输类型。主要有SOCK_STREAM(流套接字、TCP),SOCK_DGRAM(数据报套接字、UDP)SOCK_RAW(原始套接字、用于其他协议)三种。

(3)protocol: 通信使用的协议。一般参数为0,除非domain和type都相同但是protocol不同的情况下才需要具体指定。

创建一个TCP套接字如下:

int server_socket = socket(PF_INET,SOCK_STREAM,0);

2. 套接字绑定地址函数 bind

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

调用成功返回0,否则返回-1。

(1)sockfd:套接字的文件描述符。

(2)addr:保存地址信息的结构体变量的地址。

(3)addrlen:结构体变量长度。一般直接用sizeof(addr)。

定义addr结构体并赋值如下:

struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);
server_addr.sin_port = htons(atoi(server_port));

结构体struct sockaddr_in中有四个变量:

sin_family,保存地址族信息,常用的有AF_INET(ipv4)、AF_INET6(ipv6);

sin_addr.s_addr,保存32位IP地址,inet_addr()函数将点分十进制的字符串IP地址转化为整数型,同时进行网络字节序转换。 为了统一网络传输数据的顺序,规定采用大端序传输,所以字段要经过网络字节序转换。

sin_port,保存16位端口号,htons()函数用于网络字节序转换,htons意为host to network short(用于16位端口),htonl意为host to network long(用于32位地址)。

sin_zero[8],不使用,必须填充0。所以使用memset()函数给结构体赋初值0。

调用bind函数如下:

bind(server_socket, (struct sockaddr* )&server_addr, sizeof(server_addr));

为什么bind函数第二个参数要求sockaddr结构体,却传入 sockaddr_in结构体?

因为sockaddr结构体将所有信息全部保存在一个数组中,不方便进行赋值。

3. 等待连接请求函数 listen

int listen(int sock, int backlog);

调用成功返回0,失败返回-1。

(1)sock:监听套接字的文件描述符。

(2)backlog:连接请求等待队列的长度。当客户端发起连接请求时会进入连接请求等待队列,服务端通过队列的顺序依次进行连接。

调用listen函数如下:

listen(server_socket,5);

4. 处理连接请求函数 accept

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

调用成功返回套接字文件描述符,失败返回-1。

(1)sock:服务器套接字的文件描述符。

(2)addr:客户端地址信息的结构体变量地址。函数会自动计算连接的客户端的地址信息保存在此结构体变量中。

(3)addrlen:第二个参数长度的变量地址。

调用accept函数如下:

client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&client_len);

5. 关闭套接字函数 close

int close(int sockfd);

服务端在accept后会创建一个clientsocket和客户端的clientsocket相连接,所以客户端和服务端都要close这个clientsocket。

二、客户端函数

1. 请求连接函数 connect

int connect(int sock, struct sockaddr* addr, socklen_t addrlen);

 调用成功返回0,失败返回-1。

(1)sock:客户端套接字文件描述符

(2)addr:保存服务器端地址信息的结构体变量的地址。在客户端代码中,同样要进行服务端地址的初始化。

(3)addrlen:结构体变量长度。

三、完整代码

使用上述函数编写一个简单的TCP客户端和服务端,客户端发送一个字符串,服务端接收并打印,客户端输入q则断开连接。

服务端代码:

#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
using namespace std;
int main()
{
    char buf[1024];

    int server_socket;
    int client_socket;
    server_socket = socket(PF_INET,SOCK_STREAM,0);
    if(server_socket == -1)
    cout << "socket error" << endl;

    char* server_ip = "127.0.0.1";
    char* server_port = "9955";
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(server_ip);
    server_addr.sin_port = htons(atoi(server_port));

    if(bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1)
    cout << "bind error" <<endl;

    if(listen(server_socket,5) == -1)
    cout << "listen error" << endl;

    client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&client_len);
    if(client_socket == -1)
    cout << "accept false" << endl;
    
    while(recv(client_socket,buf,sizeof(buf),0))  //当客户端close后,发送EOF,退出循环
    {
        send(client_socket,buf,sizeof(buf),0);
        cout << buf << endl;
    }

    close(client_socket);
    close(server_socket);
    return 0;
}

客户端代码:

#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
using namespace std;

int main()
{
    int client_socket;
    struct sockaddr_in server_addr;
    char* server_ip = "127.0.0.1";
    char* server_port = "9955";
    char buf[1024];

    client_socket = socket(PF_INET,SOCK_STREAM,0);

    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(server_ip);
    server_addr.sin_port = htons(atoi(server_port));

    if(connect(client_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))==-1)
    cout << "connect error" << endl;
    
    int strlen,recvlen;
    while(1)
    {
        cout << "input: ";
        cin >> buf;
        if(!strcmp(buf,"q"))
        break;
        strlen = send(client_socket,buf,sizeof(buf),0);

        recvlen = 0;
        while(recvlen < strlen)
        recvlen += recv(client_socket,&buf[recvlen],sizeof(buf),0);    //确保数据接收完全
    }
    close(client_socket);
    return 0;
}

四、 基于TCP的半关闭 shutdown

int shutdown(int sock, int howto);

调用成功返回0,失败返回-1。

(1)sock:套接字文件描述符

(2)howto:断开方式。SHUT_RD断开输入流、SHUT_WR断开输出流、SHUT_RDWR同时断开输入输出流。

TCP连接建立后,两端点之间会产生两条输出到输入的流,close函数会直接关闭两条流,但是shutdown函数可以选择只关闭其中一条流。比如端点发送完毕数据,则关闭输出流,向另一方发送一个EOF消息,此时不能再发数据了,但是可以继续接收另一方发来的数据。

五、 套接字可选项 getsockopt & setsockopt

上面我使用的套接字都是默认套接字,但是套接字的属性也可以修改。

读取可选项信息函数getsockopt:

int getsockopt(int sock, int level, int optname, 
                void* optval, socklen_t* optlen);

 修改可选项信息函数setsockopt:

int setsockopt(int sock, int level, int optname, 
                const void* optval, socklen_t optlen);

(1)sock:套接字文件描述符

(2)level:可选项协议层。如SOL_SOCKET、IPPROTO_TCP等

(3)optname:可选项名。可选项协议层中包含的选项名

(4)optval:存储选项信息的地址

(5)optlen:optval的字节数

在此我用可选项SO_REUSEADDR举例。

我在运行服务端代码时遇到了一个问题,如果服务端非正常关闭,再次运行服务端时会报错:地址绑定错误。我查看端口状态发现端口正处于time_wait。

这种情况出现的原因是:先发送fin消息的主机在断开连接后会进入time_wait状态。这样如果另一方未收到确认终止连接的包时,可以重传确认包,保证另一方收到确认包正常终止。

但是这个time_wait状态往往会影响我们测试代码,所以可以设置套接字的可选项,让其可以绑定处于time_wait状态的端口号。

函数调用如下:

int opt = true;
socklen_t optlen = sizeof(opt);
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void* )&opt,optlen);

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值