网络基础四 之Socket通信

1 TCP/IP回顾

TCP/IP协议参考OSI模型把所有的TCP/IP协议归类到四个抽象层中。

应用层: TFTPHTTPSNMPFTPDNSTelnet

传输层: TCPUDP

网络层: IPICMPIGMPEIGRP

链路层: SLIPCSLIPPPPMTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务。



TCP/IP协议中两个因特网主机通过两个路由器和对应的层连接。各主机上的应用通过一些数据通道相互执行读取操作。

2 网络之间的通信

本地的进程间通信(IPC)有很多种方式,可以总结为下面4类:

1)消息传递(管道、FIFO、消息队列)

2)同步(互斥量、条件变量、读写锁、文件和写纪录锁、信号量)

3)共享内存(匿名的和具体的)

4)远程过程调用(Solaris门和Sun RPC)

网络之间如何通信,首先解决的问题是如何能够唯一标识一个进程。在本地进程通信可以通过PID来唯一标识一个进程,但是PID只在本地唯一,网络中的两个进程PID冲突几率很大,这时候我们需要另辟它经。我们知道网络层的ip地址可以唯一标识主机,而传输层“协议+端口号”可以唯一标识主机的一个应用程序(进程),这样我们可以利用ip地址+协议+端口号”唯一标识网络中的一个进程。网络中的进程通信就可以利用这个标志与其他进程进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。

能够唯一标识网络中的进程后,他们就可以使用socket进行通信了。什么是socket呢?

3 Socket

Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“open->读写write/read->关闭close”模式操作。Socket是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

我们把socket翻译为套接字,socket是应用层和网络层之间的一个抽象层,他把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。


Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

4 socket通信流程


1)服务器根据地址类型(ipv4、ipv6)、socket类型、协议创建socket

2)服务器为socket绑定IP地址和端口号

3)服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开

4)客户端创建socket

5)客户端打开socket,根据服务器IP地址和端口号试图连接服务器的socket

6)服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求。

7)客户端连接成功,向服务器发送连接状态信息

8)服务器accept()方法返回,连接成功

9)客户端想socket写入信息

10)服务器读取信息

11)客户端关闭

12)服务器端关

5 Socket中TCP的我三次握手四次分手

5.1 TCP三次握手

 

第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列号 Synchronize Sequence Number),syn = j,客户端进入SYN_SEND状态等待服务器确认;

第二次握手:服务器接收客户端syn包并确认(ack=j+1),同时向客户端发送一个SYN包,此时服务器进入SYN_RECV状态;

第三次握手:客户端接收服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

服务器socket与客户端socket建立连接的部分就是大名鼎鼎的三次握手

5.2 socket中发送的TCP三次握手


从上图可以看出,1)当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时候connect进入阻塞状态;

2)服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K,ACK J+1,这时accept进入阻塞状态;

3)客户端收到服务器的SYN K,ACK J+1之后,这时会connect返回,并对SYN K进行确认;

4)服务器收到ACK K+1时,accept返回。至此三次握手完毕,并建立连接。

总结:客户端的connect在三次握手的第二次返回,而服务器端的accept在三次握手的第三次返回。

5.3 socket中发送的TCP四次分手


图示过程如下:

1)某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

2)另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

3)一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

4)接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

6 socket基本函数

6.1 socket()函数

socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一socket描述符socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

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

功能:根据指定的地址族、数据类型和协议来分配一个socket的描述字及其所用的资源。

domain:协议族。常用的有AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE。其中AF_INET代表使用的IPv4地址;

type:socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等;

protocol:协议。常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_TIPC等。

注意:1)并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

2)当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

6.2 bind()函数

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

功能:把一个地址族中特定地址赋值给socket。

sockfd:socket描述字,也就是socket的引用;

addr:要绑定给sockfd的协议地址;

addrlen:地址的长度

通常服务器在启动的时候都会绑定一个总所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来连接服务器;而客户端就不用指定,有系统自动分配一个端口号合自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机产生一个。

6.3 listen()函数、connect()函数

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器就会接受到这个请求。

int listen(int sockfd, inr backlog);

功能:监听socket

sockfd:要监听的socket描述字;

backlog:相应socket可以排队的最大连接个数;

socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

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

功能:连接某个socket

sockfd:客户端的socket描述字

addr:服务器的socket地址

addrlen:socket地址的长度

客户端通过调用connect函数来建立与TCP服务器的连接。

6.5 accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后就会调用accept()函数去接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

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

功能:TCP服务器监听到客户端请求之后,调用accept()函数接收请求。如果accept成功,那么气返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

sockfd:服务器的socket描述字

addr:客户端的socket地址

addlen:socket地址长度

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为socket描述字;而accept()函数返回的是已连接的socket描述字。一个服务器通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在,内核为每个服务器进程接收的客户连接创建了一个已经连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭了。

6.6 read()write()函数

服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()

recv()/send()

readv()/writev()

recvmsg()/sendmsg()

recvfrom()/sendto()

推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

       #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);

       ssize_t write(int fd, const void *buf, size_t count);

       #include <sys/types.h>

       #include <sys/socket.h>

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

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

       ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,

                      const struct sockaddr *dest_addr, socklen_t addrlen);

       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,

                        struct sockaddr *src_addr, socklen_t *addrlen);

       ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

ssize_t read(int fd, void *buf, szie_t count)

功能:读取socket内容。当读取成功时,read返回实际所读的字节数,如果返回的是0表示已经读到文件的结尾了,小于0表示读取出现错误。如果错误为EINTR说明是由中断引起的,如果是ECOMMREST表示网络连接串线了问题。

fd:socket描述字

buf:缓冲区

count:缓冲区长度

ssize_t write(int fd, const void *buf, size_t count)

功能:write()函数将buf的nbytes字节内容写入文件描述符fd(向socket写入内容,其实就是发送内容)。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能:1)write的返回值大于0,表示写了文部分或者全部的数据;2)返回值小于0,此时出现错误。如果错误为EINTR表示在写的时候出现了中断错误;如果是EPIPE表示网络连接出现了问题(对方已经关闭了连接)

fd:socket描述字

buf:缓冲区

count:缓冲区长度

6.7 close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成读写操作就要关闭相应的socket描述字。

#include <unistd.h>

int close(int fd)

功能:close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能在作为read或write的第一个参数。

注意:close操作只是使相应socket描述字的引用计数-1,当引用计数为0的时候吗,触发TCP客户端向服务器发送终止连接请求。

7 例子

编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。

rech.h

#ifndef __RECV__H

#define __RECV__H

#define BUFSIZE 1024

void my_err(const char * err_string, int line);

int my_recv(int conn_fd, char *data_buf, int len);

#endif

recv.c

#define MY_RECV_C

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include <errno.h>

#include "recv.h"

/* 自定义的错误处理函数*/

void my_err(const char * err_string, int line)

{

fprintf(stderr, "line: %d ", line);

perror(err_string);

exit(1);

}

/*

函数名: my_recv

 述:从套接字读取一次数据(以'\n'为结束标志)

 数:con_fd--从连接套接字上接收数据

data_buf--读取到的数据保存在此缓冲中

len--data_buf所指向的空间长度

返回值:出错返回-1,服务器端已关闭连接则返回0,成功返回读取的字节数

*/

int my_recv(int conn_fd, char *data_buf, int len)

{

       static char recv_buf[BUFSIZE]; // 自定义缓冲区,BUFSIZE定义在my_recv.h中

       static char *pread; // 指向下一次读取数据的位置

       static int len_remain = 0; // 自定义缓冲区中剩余字节数

       int i;

// 如果自定义缓冲区中没有数据,则从套接字读取数据

if (len_remain < 0)

{

if (len_remain = recv(conn_fd, recv_buf, sizeof(recv_buf), 0) < 0)

{

my_err("recv", __LINE__);

}

else if (len_remain == 0) // 目的计算机端的socket关闭

{

return 0;

}

pread = recv_buf; // 重新初始化pread指针

}

// 从自定义缓冲区读取一次数据

for (i = 0; *pread != '\n'; i++)

{

if (i > len) // 防止指针越界

{

return -1;

}

data_buf[i] = *pread;

len_remain --;

}

// 去除结束标志

len_remain--;

pread++;

return i; //读取成功

}

server.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include "recv.h"

#define SERV_PORT 4507 // 服务器端口

#define LISTENQ 12 // 连接请求队列的最大长度

#define INVALID_USERINFO 'n' // 用户信息无效

#define VALID_USERINFO 'y' // 用户信息有效

#define USERNAME 0 // 接收到的是用户名

#define PASSWORD 1 // 接收到时密码

// 保存用户名和密码的结构体

struct userinfo

{

char username[32];

char password[32];

};

struct userinfo users[] = { {"linux", "inix"},

{"4507", "4508"},

{"clh", "clh"},

{"xl", "xl"},

{" ", " "} //以只含一个空格的字符串作为数组的结束标志

  };

// 查找用户名是否存在,存在返回该用户名的下标,不存在则返回-1,出错返回-2

int find_name(const char *name)

{

int i;

if (name == NULL)

{

printf("in find_name, NULL pointer");

return -2;

}

for (i = 0; users[i].username[0] != ' '; i++)

{

if (strcmp(users[i].username, name) == 0)

{

return i;

}

}

return -1;

}

// 发送数据

void send_data(int con_fd, const char *string)

{

if (send(con_fd, string, strlen(string), 0) < 0)

{

my_err("send: ", __LINE__); //my_err函数在my_recv中声明

}

}

int main()

{

int sock_fd, conn_fd;

int optval;

int flag_recv = USERNAME; // 标识接收到的是用户名还是密码

int ret;

int name_num;

pid_t pid;

socklen_t cli_len;

struct sockaddr_in cli_addr, serv_addr;

char recv_buf[128];

//创建一个套接字

sock_fd = socket(AF_INET, SOCK_STREAM, 0);

if (sock_fd < 0)

{

my_err("socket: ", __LINE__);

}

// 设置该套接字使之可以重新绑定端口

optval = 1;

if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(int)) < 0)

{

my_err("setsockopt: ", __LINE__);

}

// 初始化服务器端地址结构

memset(&serv_addr, 0, sizeof(struct sockaddr_in));

    serv_addr.sin_family = AF_INET;

    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    serv_addr.sin_port = htons(SERV_PORT);

// 将套接字绑定到本地端口

if (bind(sock_fd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) < 0)

    {

     my_err("setsockopt: ", __LINE__);       

    }

// 将套接字转化为监听套接字

if (listen(sock_fd, LISTENQ) < 0)

{

my_err("setsockopt: ", __LINE__);

}

cli_len = sizeof(struct sockaddr_in);

while(1)

{

// 通过accept接收客户端的连接请求,并返回连接套接字用于收发数据

conn_fd = accept(sock_fd, (struct sockaddr*)&cli_addr, cli_len);

if (conn_fd < 0)

{

my_err("setsockopt: ", __LINE__);

}

printf("accept a new client, ip: %s\n", inet_ntoa(cli_addr.sin_addr));

// 创建一个子进程处理刚刚接收的连接请求

if ( (pid = fork()) == 0) // 子进程

{

while(1)

{

if ((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0)

{

perror("recv");

exit(1);

}

recv_buf[ret - 1] = '\0'; // 将数据结束标志'\n'替换成字符串标识符

if (flag_recv == USERNAME)

{

name_num = find_name(recv_buf);

switch (name_num)

{

case -1:

send_data(conn_fd, "n\n");

break;

case -2:

exit(1);

break;

default:

send_data(conn_fd, "y\n");

flag_recv = PASSWORD;

break;

}

}

else if(flag_recv == PASSWORD) // 接收的是密码

{

if (strcmp(users[name_num].password, recv_buf) == 0)

{

send_data(conn_fd, "y\n");

send_data(conn_fd, "Welcome login my tcp server\n");

printf("%s login \n", users[name_num].username);

break;

}

else

send_data(conn_fd, "n\n");

}

}

close(sock_fd);

close(conn_fd);

exit(0); // 结束子进程

}

else // 父进程关闭刚刚接收的连接请求,执行accept等待其他连接请求

close(conn_fd);

}

return 0;

}

Client.h

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <errno.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include "recv.h"

#define INVALID_USERINFO 'n' // 用户信息无效

#define VALID_USERINFO 'y' // 用户信息有效

// 获取用户输入存入到buf,buf的长度为len,用户输入数据以'\n'为结束标志符

int get_userinfo(char *buf, int len)

{

int i;

int c;

if (buf == NULL)

return -1;

i = 0;

while(((c = getchar()) != '\n') && (c != EOF) && (i < len - 2))

{

buf[i++] = c;

}

buf[i++] = '\n';

buf[i++] = '\0';

return 0;

}

// 输入用户名,然后通过fd发送出去

void input_userinfo(int conn_fd, const char *string)

{

char input_buf[32];

char recv_buf[BUFSIZE];

int  flag_userinfo;

// 输入用户的信息指导正确为止

do

{

printf("%s: ", string);

if(get_userinfo(input_buf, 32) < 0)

{

printf("error return from get_userinfo\n");

exit(1);

}

if (send(conn_fd, input_buf, strlen(input_buf), 0) < 0)

{

my_err("send", __LINE__);

}

// 从连接套接字读取上读取一次数据

if (my_recv(conn_fd, recv_buf, sizeof(recv_buf)) < 0)

{

printf("data is too long\n");

exit(1);

}

if (recv_buf[0] == VALID_USERINFO)

{

flag_userinfo = VALID_USERINFO;

}

else

{

printf("%s error, input again.", string);

flag_userinfo = INVALID_USERINFO;

}

}while(flag_userinfo == INVALID_USERINFO);

}

int main(int argc, char** argv)

{

int i;

int ret;

int conn_fd;

int serv_port;

struct sockaddr_in serv_addr;

char recv_buf[BUFSIZE];

// 检查参数个数

if (argc != 5)

{

printf("Usage: [-p] [serv_port] [-a] [serv_address]\n");

exit(1);

}

// 初始化服务器端地址结构

memset(&serv_addr, 0, sizeof(struct sockaddr_in));

    serv_addr.sin_family = AF_INET;

// 从命令行获取服务器端的端口与地址

for (i = 1; i < argc; i++)

{

if (strcmp("-p", argv[i]) == 0)

{

serv_port = atoi(argv[i+1]);

if (serv_port < 0 || serv_port > 65535)

{

printf("invalid serv_addr.sin_port\n");

exit(1);

}

else

{

serv_addr.sin_port = htons(serv_port);

}

continue;

}

if (strcmp("-a", argv[i]) == 0)

{

if (inet_aton(argv[i+1], &serv_addr.sin_addr) == 0)

{

printf("invalid server ip address\n");

exit(1);

}

continue;

}

}

// 检查参数是否少输入了某项参数

if (serv_addr.sin_port == 0 || serv_addr.sin_addr.s_addr == 0)

{

printf("Usage: [-p] [serv_port] [-a] [serv_address]\n");

        exit(1);

}

// 创建一个socket套接字

conn_fd = socket(AF_INET, SOCK_STREAM, 0);

if (conn_fd < 0)

my_err("socket", __LINE__);

// 向服务器端发送连接请求

if(connect(conn_fd, (struct sockaddr *) &serv_addr, sizeof(struct sockaddr)) < 0)

my_err("socket", __LINE__);

// 输入用户名和密码

input_userinfo(conn_fd, "username");

input_userinfo(conn_fd, "password");

// 读取欢迎信息并打印出来

if ((ret = my_recv(conn_fd, recv_buf, sizeof(recv_buf))) < 0)

{

printf("data is too long\n");

exit(1);

}

for (i = 0; i < ret; i++)

{

printf("%c", recv_buf[i]);

}

printf("\n");

close(conn_fd);

return 0;

}

参考

简单理解Socket

http://www.cnblogs.com/dolphinX/p/3460545.html

Linux Socket编程(不限Linux)

http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html#!comments

猜你喜欢

转载自blog.csdn.net/qingzhou4122/article/details/60140157