windows - TCP通讯流程

TCP通讯流程

在这里插入图片描述

三次握手

在这里插入图片描述

TCP建立连接的过程叫做握手
握手需要在客户和服务器之间交换三个TCP报文段,称为三次握手
采用三次握手主要是为了防止已失效的连接请求报文段 突然又传送到了,因而产生错误。

第一次握手:
客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=x,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。

第二次握手:
服务器端收到数据包后由标志位SYN=1表示知道客户端请求建立连接,同时将标志位SYN和ACK都置为1,确认号ack=x+1,随机产生一个序号值seq=y,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。

第三次握手:
客户端收到确认后,检查确认号ack是否为x+1,标志位ACK是否为1,如果正确则将标志位ACK置为1,确认号ack=y+1,并将该数据包发送给服务器端,服务器端检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了

注意:我们上面写的ack和ACK,不是同一个概念:

  • 小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,ack=seq+1。
  • 大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1。

四次挥手

在这里插入图片描述

第一次挥手:
Client端发起挥手请求,并且停止发送数据,向Server端发送标志位是FIN报文段,FIN=1,设置序列号seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,Client端进入FIN_WAIT_1(终止等待1)状态,这表示Client端没有数据要发送给Server端了(TCP规定,FIN报文段即使不携带数据,也要消耗一个序号)

第二次挥手:
Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK=1的报文段,ack设为seq加1(u+1),并且带上自己的序列号seq=v,服务端就进入了CLOSE-WAIT(关闭等待)状态,TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

第三次挥手:
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

第四次挥手:
客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端依然没有收到回复,才进入CLOSED状态。

服务器只要收到了客户端发出的确认,立即进入CLOSED状态。可以看到,服务器结束TCP连接的时间要比客户端早一些。

服务器端

//绑定0端口: 分配本机空闲端口
//绑定0IP: :代表本机所有IP

WSAstartup()

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

参数wVersionRequested是Windows Sockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
lpWSAData 是指向WSADATA数据结构的指针,用来接收Windows Sockets实现的细节。
功能:使用Socket的程序在使用Socket之前必须调用WSAStartup函数。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
返回值:int类型 成功返回0

wVersionRequested 参数用来指明我们希望使用的版本号,它的类型为 WORD,等价于 unsigned short,是一个整数,所以需要用 MAKEWORD() 宏函数对版本号进行转换。例如:

MAKEWORD(1, 2);  //主版本号为1,副版本号为2,返回 0x0201
MAKEWORD(2, 2);  //主版本号为2,副版本号为2,返回 0x0202
#define MAKEWORD(a, b)      ( a | b << 8)
#define MAKELONG(a, b)      (a | b << 16)
	a=0x12
	b=0x34
	MAKEWORD(a,b)=0x3412
	a = 0x1234
	b = 0x5678
	MAKELONG(a,b)=0x56781234
    
#define LOBYTE(w)           ((BYTE)(w& 0xff))
#define HIBYTE(w)           ((BYTE)(w >> 8) & 0xff)
#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l)           ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
	x = 0x1234
	LOBYTE(x)=0x34
	HIBYTE(x)=0x12
	y = 0x12345678
	LOWORD(y)=0x5678
	HIWORD(y)=0x1234

socket

SOCKET WSAAPI socket(
  int af, //地址族规范。 地址族的可能值在Winsock2.h头文件中定义。
  int type,//新套接字的类型规范。
  int protocol//要使用的协议。
);

返回值: returns file (socket) descriptor if OK, -1 on error.(成功返回文件描述符,失败返回-1)
#define INVALID_SOCKET (SOCKET) (~0)
#define SOCKET_ERROR (~1)
参数int af
当前支持的值为AF_INET或AF_INET6,这是IPv4和IPv6的Internet地址族格式。 如果安装了地址族的Windows套接字服务提供程序,则支持地址族的其他选项(例如,与NetBIOS一起使用的AF_NETBIOS)。 请注意,(AF_地址族和PF__协议族)常量的值是相同的(例如,AF_INET和PF_INET),因此可以使用任何一个常量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0OqFENt-1686921166997)(D:\HomeWork\笔记\网络与数据库\存储图片\int af.png)]

int type
新套接字的类型规范。 AF_INET族的子协议:
套接字类型的可能值在Winsock2.h头文件中定义。
应用程序可以通过WSAEnumProtocols函数动态发现每个可用传输协议的属性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5I4Z6ro8-1686921166998)(D:\HomeWork\笔记\网络与数据库\存储图片\int type.png)]

int protocol IPPROTO_IP
要使用的协议。 protocol参数的可能选项特定于指定的地址族和套接字类型。 在Winsock2.h和Wsrm.h头文件中定义了协议的可能值。
在为Windows Vista和更高版本发布的Windows SDK上,头文件的组织已更改,并且此参数可以是Ws2def.h头文件中定义的IPPROTO枚举类型的值之一。 请注意,Ws2def.h头文件自动包含在Winsock2.h中,并且绝对不能直接使用。
如果指定的值为0,则调用者不希望指定协议,服务提供商将选择要使用的协议。
当af参数为AF_INET或AF_INET6且类型为SOCK_RAW时,在IPv6或IPv4数据包头的协议字段中设置为协议指定的值。

bind()

bind() 函数的原型为:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

返回值:returns 0 on success, or -1 on error.(成功返回0,失败返回-1)

sockaddr 结构体

struct sockaddr{
    u_short  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下:

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

sockaddr_in 结构体

struct sockaddr_in{
    short     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号   1-65535
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
}SOCKADDR_IN, *PSOCKADDR_IN;
  1. sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
  2. sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。端口号需要用 htons() 函数转换。
  3. sin_addr 是 struct in_addr 结构体类型的变量。
  4. sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。

in_addr 结构体

sockaddr_in 的第3个成员是 in_addr 类型的结构体,该结构体只包含一个成员,如下所示:

typedef struct in_addr {
        union {
                struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { USHORT s_w1,s_w2; } S_un_w;
                ULONG S_addr;
        } S_un;
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

in_addr_t 在头文件 <netinet/in.h> 中定义,等价于 unsigned long,长度为4个字节。s_addr 是一个整数,而IP地址是一个字符串,所以需要 inet_addr() 函数进行转换。

unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);
sockaddr_in addr_in = { AF_INET,htons(0x1234) };
addr_in.sin_addr;

listen()

int WSAAPI listen(
    SOCKET s,
    int backlog
);

参数:s:指定接收端套接字描述符,客户端的socket,每个客户对应唯一的socket;
backlog:规定了内核应该为相应套接字排队的最大连接个数。用SOMAXCONN则为系统给出的最大值
挂起连接队列的最大长度。 如果设置为 SOMAXCONN,则 负责套接字的基础 服务提供商会将积压工作设置为最大合理值。 如果设置为 SOMAXCONN_HINT (N) ((其中 N 为数字) ),积压工作值将为 N,调整为 (介于 200、65535) 范围内。 请注意, SOMAXCONN_HINT 可用于将积压工作设置为大于 SOMAXCONN 的值。
SOMAXCONN_HINT 仅受 Microsoft TCP/IP 服务提供商支持。 没有用于获取实际积压工作值的标准预配。返回值:
功能:listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。

调用listen导致套接字从CLOSED状态转换到LISTEN状态。

backlog:
Linux内核协议栈为一个tcp连接管理使用两个队列,分别是:
半连接队列:用来保存SYN_SENT和SYN_RECV两个状态的连接,也就是三次握手还没有完成,连接还没建立的连接。
全连接队列:用来保存保存ESTABLISHED状态的连接。三次握手已经完成的连接。
全连接队列存放三次握手成功的连接。如果当服务器不调用accept函数,没有将全连接队列的请求拿出来,当该队列满的时候,客户端的连接就无法再过来,而是存放在半连接队列中,所以当全连接满时,服务器会处于SYN_RECV状态。

注意:全队列的长度是listen的第二个参数+1

内核在判定全连接队列为满时采用的是>而不是>=,我们listen的第二个参数为sk_max_ack_backlog表示全连接队列最大长度,只有当前全连接队列长度sk_ack_backlog比sk_max_ack_backlog大时,才判定全连接队列已满。

accept()

SOCKET WSAAPI accept(
    SOCKET s,
    struct sockaddr FAR * addr,
    int FAR * addrlen
);

参数:s:一个描述符,用于标识已使用 侦听 函数处于侦听状态的套接字。 连接实际上是使用 接受返回的套接字建立的。
addr:指向接收连接实体地址(称为通信层)的缓冲区的可选指针。 添加器参数的确切格式由创建 sockaddr结构中的套接字时建立的地址系列确定。
addrlen:指向包含 addr 参数指向的结构长度的整数的可选指针。
返回值:成功accept将返回一个类型为 SOCKET 的值,该值是新套接字的描述符。 此返回的值是实际连接的套接字的句柄。失败返回 INVALID_SOCKET
功能:允许在套接字上尝试传入连接

recv()

int recv( SOCKET s, char *buf, int len, int flags);

参数:s 指定接收端套接字描述符,客户端的socket,每个客户对应唯一的socket;
buf 指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
客户端消息的存储空间,也就是字符数组
这个一般1500个字节,也是网络传输的最大单元,也就是客户端发过来的数据,是协议规定的,这个数据也是根据很多情况总结出来的最优值;
len 参数指明buf的长度,想要读取的字节个数,一般是参数2的字节数-1,把0字符串结尾留出来;
flags 一组影响此功能行为的标志,数据的读取方式,填0。
返回值:成功的话返回读出来字节数的大小
如果客户端下限了,返回0
如果没有系统没有收到客户端发送的数据,recv函数就停着等数据
执行失败返回SOCKET_ERROR,用函数WSAGetLastError()得到错误码,根据得到的错误码作相应处理

recvfrom()

recvfrom(
    SOCKET s,
    char FAR * buf,
    int len,
    int flags,
    struct sockaddr FAR * from,
    int FAR * fromlen
);

参数解释: sockfd – 接收端套接字描述;
buf – 用于接收数据的应用缓冲区地址;
len – 指明缓冲区大小;
flags – 通常为0(数据的接收有的时候不满足条件,client端并没有给server端发送数据,此时server端应该处于阻塞状态等待);
src_addr – 数据来源端的地址(IP address,Port number)(因为我作为接收端希望知道谁给我发的,发的什么).
fromlen – 作为输入时,fromlen常常设置为sizeof(struct sockaddr)(要知道发送过来的大小).
返回值:成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中

注意:accept与recvfrom:都能获取对方信息:
getpeername和recvfrom,在tcp的客户端或者收发数据的accept_sock都能获取对方的信息
tcp的服务器使用accept,当客户端刚刚连接成功就能获取到客户端的信息。
recvfrom在接收数据的同时,可以获对方的信息(包括udp和tcp)

客户端

WSAstartup()

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

参数wVersionRequested是Windows Sockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
lpWSAData 是指向WSADATA数据结构的指针,用来接收Windows Sockets实现的细节。
功能:使用Socket的程序在使用Socket之前必须调用WSAStartup函数。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
返回值:int类型 成功返回0

socket

SOCKET WSAAPI socket(
  int af, //地址族规范。 地址族的可能值在Winsock2.h头文件中定义。
  int type,//新套接字的类型规范。
  int protocol//要使用的协议。
);

返回值: returns file (socket) descriptor if OK, -1 on error.(成功返回文件描述符,失败返回-1)
#define INVALID_SOCKET (SOCKET) (~0)
#define SOCKET_ERROR (~1)
参数:af:地址族规范。地址系列的可能值在 Winsock2.h 头文件中定义。
type:SOCK_STREAM(1)[TCP] SOCK_DGRAM(2)[UDP]
功能:套接字函数创建绑定到特定传输服务提供程序的套接字

bind()

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen);  //Windows

参数:s:标识未绑定套接字的描述符。
addr:指向要分配给绑定套接字的本地地址的 sockaddr 结构的指针。
namelen:name 参数指向的值的长度(以字节为单位)。
返回值:returns 0 on success, or -1 on error.(成功返回0,失败返回-1)
功能绑定函数将本地地址与套接字相关联。

connect()

int WSAAPI connect(
    SOCKET s,
    const struct sockaddr FAR * name,
    int namelen
);

参数:s:标识未连接的套接字的描述符。
name:指向应建立连接的 sockaddr结构的指针。
namelen:名称参数指向的 sockaddr结构的长度(以字节为单位)。
返回值:成功返回零。错误返回SOCKET_ERROR,可以通过调用 WSAGetLastError来检索特定的错误代码。
功能连接函数建立与指定套接字的连接。
错误情况:1: 调用connect时内核发送一个SYN分节,若无响应则等待6s后再次发送一个,仍无响应则等待24s再发送一个,若总共等了75s后仍未收到响应则返回ETIMEDOUT错误;
2.:若对客户的SYN的响应是RST,则表示该server主机在我们指定的port上面没有进程在等待与之连接,比如server进程没执行,客户收到RST就立即返回ECONNREFUSED错误;
3:若客户发出的SYN在中间的某个路由上引发了一个“destination unreachable”(目的不可达)ICMP错误,客户主机内核保存该消息,并按1中所述的时间间隔发送SYN,在某个规定的时间(4.4BSD规定75s)仍未收到响应,则把保存的ICMP错误作为EHOSTUNREACHENETUNREACH错误返回给进程。
若connect失败则该套接字不再可用,必须关闭,我们不能对这种套接字再次调用connect函数。
在每次connect失败后,都必须close当前套接字描写叙述符并又一次调用socket。

send()

只能用于TCP,只能想连接好的固定主机发送

send(
    SOCKET s,
    const char FAR * buf,
    int len,
    int flags
    );

返回值:成功返回发送的字节数,错误则会返回-1;
类型:ssize_t类型在之前的read/write中描述符哦,相当于long。
参数: s:接收消息的套接字的文件描述符。
buf:要发送的消息。
len:要发送的字节数。
flags:flags参数表示下列标志中的0个或多个

MSG_CONFIRM :用来告诉链路层,
MSG_DONTROUTE:不要使用网关来发送数据,只发送到直接连接的主机上。通常只有诊断或者路由程序会使用,这只针对路由的协议族定义的,数据包的套接字没有。
MSG_DONTWAIT :启用非阻塞操作,如果操作阻塞,就返回EAGAIN或EWOULDBLOCK
MSG_EOR :当支持SOCK_SEQPACKET时,终止记录。
MSG_MORE :调用方有更多的数据要发送。这个标志与TCP或者udp套接字一起使用
MSG_NOSIGNAL :当另一端中断连接时,请求不向流定向套接字上的错误发送SIGPIPE ,EPIPE 错误仍然返回。
MSG_OOB:在支持此概念的套接字上发送带外数据(例如,SOCK_STREAM类型);底层协议还必须支持带外数据

获取IP和PORT

getsockname()

int WSAAPI getsockname(
        SOCKET   s,
  		struct sockaddr FAR * name,
  		int FAR * namelen
);

参数:s :标识套接字的描述符。
name:指向接收套接字) 地址 (名称的 SOCKADDR结构的指针。
namelen:名称缓冲区的大小(以字节为单位)。
返回值:如果未发生错误, 则 getsockname返回零。否则返回-1
功能:检索套接字的本地名称。IP和端口。(获取本地协议地址)(UDP)
注意:必须在之前绑定或者是TCP连接
1、在发送和接收之前,根据bind指定ip和port获得结果;
2、如果端口bind时指定0,可以获取到系统已分配的端口;
3、在发送和接收之前,没有执行bind就会调用失败
4、在发送和接受之后,即使没有执行bind也能获取本机信息。

getpeername()

int getpeername(
		 SOCKET   s,
		 struct sockaddr FAR * name,
      	 int FAR * namelen
);

参数:s :标识套接字的描述符。
name:指向接收套接字) 地址 (名称的 SOCKADDR结构的指针。
namelen:名称缓冲区的大小(以字节为单位)。
返回值:如果未发生错误, getpeername 将返回零。否则返回-1
功能:检索连接到套接字的对等方的地址。(获取外地协议地址)

注意:1、获取本机和对方的IP地址:
getpeername只能用于TCP,服务器或者客户端都可以使用;
getsockname可以用于udp和tcp:
udp如果绑定的是0IP,显示的本机ip为:0.0.0.0
getsockname用于tcp的侦听sock,如果绑定的是0IP,
显示的本机ip为:0.0.0.0
getpeername用于tcp的侦听sock,会获取失败。
getpeername用于tcp的连接成功之后的收发accept_socka,
可以显示出对方的真实IP和端口。
2、getpeername和getsockname在tcp 的客户端使用:
在建立连接之前
getsockname获取本机信息:0.0.0.0-55500
getpeername获取不到对方信息!
在建立连接之后可以获取本机和对方的地址信息:
对方信息:192.168.193.1 - 8899
本机信息:192.168.193.1 - 55500

cp:
udp如果绑定的是0IP,显示的本机ip为:0.0.0.0
getsockname用于tcp的侦听sock,如果绑定的是0IP,
显示的本机ip为:0.0.0.0
getpeername用于tcp的侦听sock,会获取失败。
getpeername用于tcp的连接成功之后的收发accept_socka,
可以显示出对方的真实IP和端口。
2、getpeername和getsockname在tcp 的客户端使用:
在建立连接之前
getsockname获取本机信息:0.0.0.0-55500
getpeername获取不到对方信息!
在建立连接之后可以获取本机和对方的地址信息:
对方信息:192.168.193.1 - 8899
本机信息:192.168.193.1 - 55500

案例

Server
#include <WinSock2.h>
#include <iostream>
using namespace std;
enum {PORT = 8899};
#pragma comment(lib,"ws2_32.lib")
int main()
{
	WSADATA w;
	if (WSAStartup(MAKEWORD(2, 3), &w))
	{
		cout << "初始化socket错误:" << WSAGetLastError() << endl;
		return -1;
	}

	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == INVALID_SOCKET)
	{
		cout << "创建socket错误:" << WSAGetLastError() << endl;
		return -1;
	}

	sockaddr_in addr_in= { AF_INET ,htons(PORT)};
	int n = bind(sock, (const sockaddr*)&addr_in, sizeof(addr_in));
	if (n)
	{
		cout << "绑定失败:" << WSAGetLastError() << endl;
		return -1;
	}

	listen(sock,5);//积压数目5
	char s[256];
	//用来显示客户端连接者的IP和PORT  这个socket用来跟客户端收发数据
	//后面两个参数类似与recvfrom的后两个参数
	SOCKET accept_sock;
	while ((accept_sock = accept(sock, nullptr, nullptr)) != INVALID_SOCKET)
	{
		while (true)
		{
			n = recv(accept_sock, s, sizeof(s), 0);
			if (n <= 0 || n >= sizeof(s))
			{
				break;
			}
			s[n] = 0;
			cout << s << endl;
		}
		
	}
	closesocket(sock);
	return 0;
}
Client
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WinSock2.h>
#include <iostream>
enum {PORT = 8899};
#pragma comment(lib,"ws2_32.lib")
using namespace std;

int main()
{
	WSADATA w;
	if (WSAStartup(MAKEWORD(2, 2), &w))
	{
		cout << "初始化socket错误:" << WSAGetLastError() << endl;
		return -1;
	}
	SOCKET sock = socket(AF_INET, SOCK_STREAM,0);
	if (sock == INVALID_SOCKET)
	{
		cout << "创建socket错误:" << WSAGetLastError() << endl;
		return -1;
	}

	sockaddr_in addr_in = { AF_INET };//0端口系统自动分配
	int n = bind(sock, (const sockaddr*)&addr_in, sizeof(addr_in));
	if (n)
	{
		cout << "绑定失败:" << WSAGetLastError() << endl;
		return -1;
	}
	int len = sizeof(addr_in);
	n = getsockname(sock, (sockaddr*)&addr_in, &len);
	if (!n)
	{
		cout << "本机信息:" << inet_ntoa(addr_in.sin_addr) << " - " << htons(addr_in.sin_port) <<endl;
	}
	n = getpeername(sock, (sockaddr*)&addr_in, &len);
	if (n)
	{
		cout << "在建立连接之前,获取不到对方信息!"<<endl;
	}
	else
	{
		cout << "对方信息:" << inet_ntoa(addr_in.sin_addr) << " - " << htons(addr_in.sin_port) << ":";
	}

	addr_in.sin_port = htons(PORT);
	addr_in.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	n = connect(sock, (const sockaddr*)&addr_in, sizeof(addr_in));
	if (n)
	{
		cout << "连接失败:" << WSAGetLastError() << endl;
		return -1;
	}

	if (n)
	{
		cout << "在建立连接之前,获取不到对方信息!";
	}
	else
	{
		cout << "对方信息:" << inet_ntoa(addr_in.sin_addr) << " - " << htons(addr_in.sin_port) << endl;
	}
	n = getsockname(sock, (sockaddr*)&addr_in, &len);
	if (!n)
	{
		cout << "本机信息:" << inet_ntoa(addr_in.sin_addr) << " - " << htons(addr_in.sin_port) << ":";
	}

	char s[256];
	while (true)
	{
		cin >> s;
		send(sock, s, strlen(s), 0);
	}
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
一、 系统概要 1、 开发平台: Windows XP, Visual C++ 6.0 2、 系统简单描述: 这是一个用VC做的简单的聊天系统。在此系统中,采用TCP协议,程序分为服务器端各客户端。系统能实现简单的聊天室功能包括一对多,而且同时也支持一对一,即私聊功能。在系统运行中,用户可以随时更改自己的用户名。功能虽然单一但也能够满足聊天的基本功能。 二、 通信规范的制作 程序中定义了一个结构体MYMESSAGE来作为信息传送的载体 struct MESSAGE { char flag; //作为标志来区别所发送的信息; char data[100];//发送的内容; char addr[25]; //客户IP地址和端口号 }; 三、 主要模块的设计分析 服务器端: 系统流程如下: 设计思路: 基于TCP/IP的通信基本上都是利用SOCKET套接字进行数据通讯,程序一般分为服务器端和用户端两部分。设计思路(VC6.0下): 第一部分 服务器端 一、创建服务器套接字(create)。 二、服务器套接字进行信息绑定(bind),并开始监听连接(listen)。 三、接受来自用户端的连接请求(accept)。 四、开始数据传输(send/receive)。 五、关闭套接字(closesocket)。 第二部分 客户端 一、创建客户套接字(create)。 二、与远程服务器进行连接(connect),如被接受则创建接收进程。 三、开始数据传输(send/receive)。 四、关闭套接字(closesocket)。
简单说明 工具支持:串口通讯、串口代理、TCP、UDP、Telnet、Ping、TFtp等通讯测试 1、本工具支持固定预定义命令,命令可以进行分组,由树形控件管理。点击“命令编辑”即可编辑预定义命令, 编辑保存后点击“命令更新”按钮,新命令即显示在左侧“命令树”中,预定义命令支持ASCII码字符串格式 、十六进制格式与转义(混合)命令格式(如"abc\r\n12345\xAB \x45"); 2、支持动态命令,如命令中含有帧长度、校验等数据项目,命令可以进行分组,由树形控件管理。方法上采用 Lua脚本语言进行扩展,扩展的界面内容包含终端窗口的右键菜单、脚本窗口的按钮事件、扩展的树命令; 3、支持命令终端模式,功能类似Windows下的超级终端,可以进行复制、粘贴等操作,可以上下翻页滚动; 在终端窗口可以通过鼠标滚轮上下滚动,可以通过“Ctrl+箭头”、“Ctrl+翻页键”上下翻页操作; 4、工具支持ZModem协议进行文件下载、上传,方便与Linux终端间交换数据; 5、支持用Lua脚本对接收到的数据进行解析,支持用脚本语言直接进行发送操作; 6、支持数据触发模式,当串口(Socket)接收到预定义格式的数据时,可以直接触发脚本内指定的函数事件; 7、支持串口回环模式,发出的数据自动返回,便于脚本调试; 8、支持常用工具嵌入到本界面内部,由脚本定制具体程序名称,如计算器、Dnw、记事本、命令行等,在操作上 形同一个整体; 9、具有串口数据记录功能,每天记录一个文件,可以加上时间标记,方便查看历史操作、历史数据; 10、可以采用脚本进行定时发送、循环发送、总线循访等操作流程; 11、Lua脚本支持发送字节到串口(Socket)、发送字符串到串口(Socket)、延时、定时器、事件触发、界面定制等操作; 12、支持命令行内容自动滚动记录功能,方便使用历史命令; 13、支持窗口总在最前模式(top on most),方便在进行其它任务,监通讯操作; 14、支持脚本扩展的语音提示功能(播放指定的Wav文件),可以用在接收到指定数据、帧错误、校验错、定时 操作完毕、超时等场合,方便提醒调试人员; 15、支持发送文件操作,包含每次发送的长度、间隔时间等,也可以通过脚本语言读取文件进行自定义格式发送, 如包含同步、长度、校验等信息。 ... ------------lua 可调用的服务函数--------------------------------- SendByte 向串口(Socket)发送一个字节 SendString 向串口(Socket)发送字符串 SendHexString 向串口(Socket)发送字符串,例子:SendHexString("12 34 56 AB 5F"); GetString 通过录入窗口获取字符串 Sleep 休眠 Wait 等待,事件继续触发 GetDateTimeStr 取得当前时间字符串 Print 打印输出信息到输出窗口 print 打印输出信息到输出窗口 PrintToCommWin 打印输出信息到串口信息窗口 PrintAsChar 打印输出信息到输出窗口(数据类型转换为字符型) ClearOutput 清除输出窗口内容 IntToChar 数据类型转换为字符型 SetButtonText 设置按钮文本 WinExec 运行外部应用程序 WinExecInWindow 运行外部应用程序(嵌入到窗口内部) GetExeDir 取得应用程序的路径 SetMenuItemText 设置终端模式下的串口(Socket)信息窗口的弹出菜单内容 GetClipboardText 获取剪贴板数据 CopyToClipboard 复制数据到剪贴板 AddLuaTreeNode 在扩展命令树中增加一个节点 PlayWave 播放语音文件 SetCmdLineText 设置命令输入行内容 GetCmdLineText 获取命令输入行内容 IntToHex 数据转换为十六进制字符串 IntToStr 数据转换为字符串 StrToInt 字符串转换为整形 bit_not 数据取反 bit_and 数据相与 bit_or 数据相或 bit_xor 数据相异或 bit_shl 数据左移 bit_shr 数据右移 GetFileName 获取文件名 inet_addr ip地址转换 SubString 取得子字符串 SetLuaTreeNodeText 设置树节点文字 SetLuaTreeNodeParam 设置树节点参数 ShowVclForm 显示扩展界面 ShowOutputForm 显示输出界面 ShowCodeForm 关闭代码窗口 HideCodeForm 关闭代码窗口 HideSendForm 关闭发送窗口 ShowSendForm 显示发送窗口 GetPathName 通过GUID获取设备名称 FileOpen 打开文件 FileSeek 移动文件指针 FileRead 读取文件 FileWrite 写文件 FileClose 关闭文件 AllocMem 分配内存 FillMem 填充1个字节到内容 GetMem 获取1字节内容内容 FreeMem 是否内存 SaveParam 保存参数到UserParam.ini GetParam 读取参数,从UserParam.ini AddBufDat 对内存内容按字节累加求和,通常用于计算校验 Update 界面刷新 ShowMessage 消息窗口 ShowLeftTools 显示左边工具栏 ShowRightTools 显示右边工具栏 ShowTerminal 显示终端窗口 HideLeftTools 隐藏左边工具栏 HideRightTools 隐藏右边工具栏 HideTerminal 隐藏终端窗口 ClearCommWin 清除通讯窗口内容 GotoCommWin 设置坐标 PrintToTerminal 打印信息到终端窗口 GetSendWinSelText 取得发送窗口中选中的数据 ClearVclControls 清除脚本语言创建的控件 ------------lua 事件----------------------------------- ReceivedByte 串口(Socket)接收到一个字节数据 ReceivedTrigData 串口(Socket)接收到特定格式数据 ProcessTrigData 串口(Socket)接收完特定格式数据后进行一次性处理 Timer100ms 100毫秒定时器事件 Timer500ms 1500毫秒定时器事件 Timer1000ms 1000毫秒定时器事件 Button1Clicked 按钮1单击事件 Button2Clicked 按钮2单击事件 Button3Clicked 按钮3单击事件 Button4Clicked 按钮4单击事件 Button5Clicked 按钮5单击事件 Button6Clicked 按钮6单击事件 Button7Clicked 按钮7单击事件 Button8Clicked 按钮8单击事件 MenuItem0Clicked 弹出菜单项0单击事件 MenuItem1Clicked 弹出菜单项1单击事件 MenuItem2Clicked 弹出菜单项2单击事件 MenuItem3Clicked 弹出菜单项3单击事件 MenuItem4Clicked 弹出菜单项4单击事件 MenuItem5Clicked 弹出菜单项5单击事件 MenuItem6Clicked 弹出菜单项6单击事件 MenuItem7Clicked 弹出菜单项7单击事件 MenuItem8Clicked 弹出菜单项8单击事件 MenuItem9Clicked 弹出菜单项9单击事件 MenuItem10Clicked 弹出菜单项10单击事件 MenuItem11Clicked 弹出菜单项11单击事件 MenuItem12Clicked 弹出菜单项12单击事件 MenuItem13Clicked 弹出菜单项13单击事件 MenuItem14Clicked 弹出菜单项14单击事件 MenuItem15Clicked 弹出菜单项15单击事件 MenuItem16Clicked 弹出菜单项16单击事件 MenuItem17Clicked 弹出菜单项17单击事件 MenuItem18Clicked 弹出菜单项18单击事件 MenuItem19Clicked 弹出菜单项18单击事件 转义字符 含义 ASCII码(16/10进制) \n 换行符(LF) 0AH/10 \r 回车符(CR) 0DH/13 \\ 反斜杠 5CH/92 \ddd 任意字符 1~3位十进制 \xhh 任意字符 1~2位十六进制 继续完善中,欢迎提出宝贵意见。 本软件版本:V1.25 作者:[email protected]
在Internet普及的今天,作为Internet工作基础的TCP/IP协议及其编程已经成了IT人业人员所要具备的基本知识与技能。打开国内外各大知名网站的招聘页面,都可以看到类似于“熟悉TCP/IP协议、掌握socket通讯开发”等字样的要求。本书就是为了满足读者在这方面知识的需求而编写的一本TCP/IP协议与基于TCP/IP编程方面的书籍。 本书有以下几个方面的特点: (1)内容的组织上按照协议原理与协议编程分为上、下篇。上篇主要介绍TCP/IP协议簇中的常用协议,下篇专门介绍网络编程知识与技能。 (2)具体在编写每一节的内容时将原理知识与实用技能融为一体。以方便读者学习。 (3)考虑到TCP/IP协议比较抽象,学习起来有一定的难度,所以全书尽量避免使用晦涩难懂专业术语,而用浅显易懂的语言说明问题,努力将书打造成一本人人都读懂书籍。 (4)初学网络程序设计的人员,往往感到网络程序设计内容多,学习进来比较复杂。针对这一问题,本书在讲解网络程序设计时,根据网络程序固有的特点,先总结了网络程序设计的通用模式,然后再举例说明网络程序的设计,使读者易于入手。 (5)Winsock函数内容多,使用起来比较复杂,针对这一问题,笔者在写作时将常用的Winsock函数分散到各种实例中去介绍,然后在最后一章将所有常用的Winsock函数一一作了较为详细说明,并在每个函数后面加入了其应用实例或使用说明。 本书分为上、下两篇内容,上篇内容包含6章,各章主要如下: 第1章:介绍了TCP/IP协议的产生、结构和工作原理,另外本章内容中还简要介绍一一下ISO/OSI RM。 第2章:介绍TCP/IP协议层次结构中网络接口层包含的内容,主要有物理层和数据链路的相关知识。 第3章:介绍TCP/IP协议层次结构中网络层及其相关知识。主要内容有IP数据报格式、IP层的功能、IP地址、ICMP协议、地址转换协议并介绍了IP的最新版本IP v6等。 第4章:介绍TCP/IP协议层次结构中传输层及其相关知识。主要内容有端口的概念、TCP协议和UDP协议的协议数据格式、协议原理和TCP协议与UDP协议的比较等内容。 第5章:介绍TCP/IP协议层次结构中应用层及其相关知识。主要内容有应用层常用协议DNS、FTP、Telnet、HTTP、POP和SMTP的格式、工作原理、协议实例等内容。 第6章:简要的介绍了一下TCP/IP协议在Windows和LINUX操作系统下的实现原理TCP/IP协议的二进制代码。 下篇包含以下6章内容: 第7章:介绍了网络程序设计有关的基础知识、一个网络程序入门实例和Winsock中编写网络程序常用的建立连接、传输数据、关闭连接等有关的函数。 第8章:介绍了TCP程序设计流程、基于C/C++的TCP程序设计实例和基于Java技术的TCP程序设计实例。 第9章:介绍了UDP程序设计流程、基于C/C++的UDP程序设计实例和基于Java技术的UDP程序设计实例。 第10章:介绍了使用MFC中提供的有关类进行网络程序设计知识。 第11章:介绍了Winsock API中各种函数的功能,并举例说明了些函数的使用方法。 本书在编写过程中得到了邮电出版和刘博等编辑的大力支持和帮助,在此表示感谢。由于作者水平有限,错漏之处在所难免,欢迎广大读者批评指正和提出宝贵的意见,可发邮件到。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值