socket实现的一个基本点对点聊天程序

时间:2021-10-08 14:46:07

多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。

服务器监听是指服务端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。 客户端请求是由客户端的套接字提出连接请求,要连接的目标是服务器端套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器套接字的地址和端口号,然后再向服务器端套接字提出连接请求。 连接确认是当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的信息发送给客户端,一旦客户端确认了此连接,连接即可建立。而服务器端继续处于监听状态,继续接收其他客户端的连接请求。 套接字(socket)是套接口描述字的简称。和文件句柄相似,SOCKET提供了一咱通讯机制,是WINDOWS的一种通

讯方式。应用程序创建了一个套接字后,就能够获得这种机制提供的网络服务功能。对于服务器来说,它提供

了监听网络的连接请求;对于客户机来说,它可以连接到一个给定的主计算机和特定的端口上。客户端和服务

器端可以通过套接字对象来发送和接收数据。套接字提供了分别基于连接的协议(TCP)等和无连接的协议

(UDP)等,以满足网络连接的可靠性、稳定性以及高速性的要求。

WINSOCK是网络编程接口,它构成了WINDOWS平台下网络编程的基础。

开放系统互连七层模型(OSI)
应用层——表示层——会话层——传输层——网络层——数据链路层——物理层

应用层:用户的应用程序与网络之间的接口
表示层:协商数据交换格式
会话层:允许用户使用简单易记的名称建立连接
传输层:提供终端到终端的可靠连接
网络层:使数据路由经过大型互联网络
数据链路层:决定访问网络介质的方式
物理层:将数据转换为可通过物理介质传送的位

TCP、UDP协议是位传输层的协议,而IP协议则是位于网络层的协议。

TCP是传输控制协议,它是一种面向连接的协议,向用户提供可靠的全双工的字节流。
TCP关心数据传输的准确性。
应用程序利用TCP进行通讯时,发送方和接收方之间会建立一个虚拟连接,通过这一连接,双方可以把数据当作

一个双向的字节流来进行交流。它就像打电话。我们从摘机拨号开始,到拨通后建立连接、进行通话,再到挂

机断开连接这一过程,正是抽象的面向连接的具体表现。首先,在开始通话前,拨号,双方响应,从而建立一

条虚拟的“链路”。只有在双方都处于活动状态,这条“链路”才会保持存在。其次,我们可以通过这条“链

路”进行双向的会话,并且在一般情况下,我们可以通过对方的回答来确定对方是否已经正确听到了我们所说

的话,这相当于面向连接协议为保证传输正确而进行的额外校验。

UDP是用户数据报协议,这是一种无连接的协议。UDP是一种不可靠的数据报协议,它不能保证每一个UDP数据报

可以到达目的地。但是,正是由于它的不可靠性,减少了数据确认的过程,所以UDP传输数据的效率比较高。
就像是邮信。我们只需封好信封,然后将其投到邮筒中即可,但是我们不能保证邮局在把信件发送出去和信件

在发送过程中没有受到伤害。
总体看来,面向连接的服务可以保证双方传递数据的正确性,但却要为此进行额外的校验,通信双方建立通信

信道也需要许多系统开销。而无连接的服务最大的优点就是速度快,因为它不需要去验证数据的完整性,也不

会数据是否已接收而操心。


IP是网际协议,自20世纪80年代以来它一直都是网际协议的主力协议,它使用32位地址,为TCP、UDP、ICMP等

协议提供传送的分组服务。

在WINDOWS网络编程中,套接字接口主要有三种类型:流式套接字、数据报套接字以及原始套接字。

流式套接字定义了一种可靠的面向连接的服务,实现了无差错无重复的顺序数据传输。对于建立在这种流式套

接字类型上的套接字来说,数据可以是双向传输的字节流,无长度限制。

数据报套字接口定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠。

原始套接字允许对低层协议如IP或ICMP直接访问,主要用于网络协议的测试,例如WINDOWS带的PING程序,就是

通过ICMP协议来实现的。
客户/服务器模式
在现在的网络应用中,通信双方最常见的交互模式便是客户/服务器模式。在这种模式下,客户向服务器发出服

务请求,服务器收到请求后为客户提供相应的服务。


客户/服务器模式通常采用监听/连接的方式实现。服务器端应用程序在一个端口监听对服务的请求,也就是说

,服务进程一直处于休眠状态,直到一个客户对这个服务提出了连接请求,此时服务线程被“唤醒”并且为客

户提供服务,即对客户的请求作出适当的反应。

面向连接的协议套接字的调用
面向连接的服务器端首先调用SOCKET()建立一个套接字S,用来监听客户端的连接请求。接着,调用bind()将此

套接字与本机地址、端口绑定起来。然后,调用listen()告诉套字S,对进来的连接进行监听并确认连接请求,

于是S被置于被动的监听模式。一个正在进行监听的套接字将给每个请求发送一个确认信息,告诉发送者主机已

经收到连接请求。当时监听套接字S实际上并不接受连接请求,在客户端请求被接受后,调用
accept()将返回一个与S具有相同属性,但不能被用来进行监听,只用来进行数据收发的数据套接字NS,作为与

客户端套接字相对应的连接的另一个端点。对于该客户端套接字后续的所有操作,都应该通过NS来完成。监听

套接字S仍然用于接收其他客户的连接请求。

面向连接的服务器一般是迸发服务器。在WINDOWS平台上,我们往往在调用accept()返回NS后,会创建一个请求

/应答执行线程,将NS作为参数之一传递给该线程,由该线程来完成客户端与服务器端复杂的请求应答工作,而

主线程会再次调用accept(),以接收新的客户端连接请求。

面向连接的客户端也会调用socket()建立一个套接字C,但使用像TCP这样的面向连接的协议时,客户端不必关

心协议使用什么样的本机地址,所以不用调用bind()。客户端调用connect()向服务器端发出连接请求,在与服

务器建立连接之后,客户端和服务器端就存在了一条虚拟的“管道”,客户端套字C和服务器端套接字NS构成了

“管道”的两个端点。客户端和服务器端通过这个“管道”进行数据交换,多次调用send()/recv()来进行请求

/应答,最终完成服务后关闭用于传输的套接字C和NS,并断开连接,结束此次会话。

面向无连接协议的套接字的调用
采用无连接协议(UDP)时,服务器一般都是面向事务的。一个请求和一个应答就完成了客户程序与服务器程序

之间的相互作用。

无边的服务器使用socket()和bind()来建立和绑定套接字S。与面向连接的服务器端不同,我们不必调用

listen()和accept(),只需要调用recvFrom()在套接字S上等待接收数据就可以了。因为是无连接的,因此网络

上任何一台机器发送的套接字S的数据都可以收到。从这一点上你可以想象,它们是无序的。

无连接的服务器一般都是迭代服务器。它们接收到一个数据报后,马上进行相应处理,直到处理完成后,才开

始下一个数据报的接收、处理。所以采用无连接协议时,客户端和服务器端的交互往往是很简单的,一问一答

或只问不答的方式很常见。

无连接的服务器端只有在停止服务时,才会关闭套接字。

无连接的客户端则更简单,只需要调用socket()建立一个套接字C,就可以利用sendto()和recvfrom()与服务器

的数据进行交换。在完成会话后调用closeSocket()关闭套接字C。

服务器与客户端通过已连接套接字进行接收与发送消息!

p2pcli.c

#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid == 0)
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        close(sock);
        kill(getppid(), SIGUSR1);
    }
    else
    {
        signal(SIGUSR1, handler);
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        close(sock);
    }


    
    return 0;

}

p2psrv.c

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {
        signal(SIGUSR1, handler);
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        printf("parent close\n");
        kill(pid, SIGUSR1);
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}

makefile:

.PHONY:clean all
CC=gcc
CFLAGS=-Wall -g
BIN=echosrv echocli echosrv2 p2psrv p2pcli
all:$(BIN)
%.o:%.c
    $(CC) $(CFLAGS) -c $< -o $@
clean:
    rm -f *.o $(BIN)