近期学完TCP/IP协议,东拼西凑写了一个简单Socket程序。在此总结一下,希望总结完成之后能领悟一些东西。
1.什么是Socket?
要了解这个问题首先来看一张图,
其实Socket,就是一组函数,它们和Unix I/O 函数结合起来,用以创建网络应用。由图可以看出Socket介于应用层和运输层之间,是一组接口。它把复杂的TCP/IP协议族隐藏在Socket接口中,给用户看到的只有一组接口。
2.进程之间的通信
学完TCP/IP协议都知道,从IP层来说,通信的两端是两台主机,但是实际上真正进行通信的实体是在主机中的进程,是这台主机的一个进程和另一个主机的一个进程之间交换数据,端到端的通信其实是应用进程之间的通信。在网络层,IP地址唯一标识一台主机,而运输层中的"协议+端口"可以唯一标识一个主机中应用进程,因此"IP地址+协议+端口"其实就可以唯一标识网络中的一个应用进程了。
3.Socket的一些基本操作
3.1 socket()函数
定义:
int socket (int domain, int type, int protocol); // 成功返回描述符, 出错返回-1
- domain是地址族常用的有AF_INET和AF_INET6分别代表IPv4和IPv6的地址。
- type为数据传输方式/套接字类型,常用的有SOCK_STREAM(流格式套接字/面向连接的套接字和SOCK_DGRAM(数据报套接字/无连接的套接字。这里又是另一块内容,简言之,TCP使用的是SOCK_STREAM,UDP使用的是SOCK_DGRAM。
- protocol表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
注意:当protocol为0时,会自动选择type类型对应的默认协议。
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,我的理解一个socket描述字就像一个特定文件一样,把它作为参数,通过它来进行一些读写操作,我认为type就相当于指定了文件读写的方法一样。
3.2 bind()函数
定义:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 成功返回0,出错返回-1
- sockfd指的就是socket()函数调用之后返回的那个socket描述符,唯一标识一个描述符。
- addr指针指的是要绑定给sockfd的的协议地址。IPv4对应的地址结构有两种。分别是:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr {
union {
struct { Uunsigned char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { unsigned short s_w1,s_w2; } S_un_w;
unsigned long S_addr;
} S_un;
};
struct sockaddr {
u_short sa_family;
char sa_data[14];
};
这两种地址有何不同呢,通过观察可以发现,这两个结构都是16字节的存储空间(因此很容易由sockaddr_in 转换成为sockaddr),但是sockaddr_in中有端口可以由程序员指定,而sockaddr中并没有可以指定端口的定义,因此建议程序员使用sockaddr_in,在需要使用sockaddr时将sockaddr_in转换为sockaddr。
3. addrlen地址的长度(这里要留个坑,是一个细节)。
bind()函数的作用是什么呢?通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
3.3 listen()函数(仅由TCP服务器调用)
定义:
int listen(int sockfd, int backlog); // 成功返回0, 出错返回-1
1.sockfd套接字,表明被监听的套接字描述符。
2.backlog,表明相应队列要排队的未完成请求的数量。(这里先留个坑,随后补上)
那么listen是怎么工作的呢?
listen把sockfd从主动套接字转化为一个被动套接字(使用socket()函数,默认生成一个主动套接字),使套接字可以接受来自客户端的请求。
3.4 accept()函数(仅由TCP服务器调用)
定义:
int accept(int listenfd, struct sockaddr*addr, int *addrlen); // 成功返回连接描述符, 出错返回-1
- listenfd,被监听的套接字才能调用accept()。
- addr,客户端的addr,是被填写的addr,因此使用前要先创建一个空addr。
- addrlen,一个指向地址长度的指针,是被写的。
注意看注释,返回的是一个已连接描述符,并不是客户端的描述符,这里要画重点了,下面说connect()的时候会详细说明。
accept的作用?accept函数等待来自客户端的连接请求到达被动描述符,然后在addr中填写客户端的套接字地址,并返回一个连接描述符。
3.4 connect()函数(由客户端发起请求)
定义:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 成功返回0, 出错返回-1
这个函数是由客户端发起的,其实不用详细说明,根据前面的解释,用于连接服务器和accept()建立联系的函数。
这里要强调的有一点,就是和accept()之间建立联系的过程,这里由一张图给出。
connfd就是accept()返回的连接套接字。
3.5 read()和write()等函数
接下来是读写函数了,有很多组,这里只介绍两组分别是
recv()和send()
int recv(int sock, char *buf, int len, int flags); // 成功返回读取的字节数,失败返回-1
int send( int sock, const char *buf, int len, int flags ); // 成功返回发送字节数,失败返回-1
- sock指定接收端套接字描述符(即连接描述符)
- buf指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
- len指明buf的长度
- flags一般置0
recvfrom()和sendto()
int recvfrom(int sock, const char *buf, int len, int flags, const struct sockaddr* from, int* fromlen) // 成功则返回接收到的字节数,失败返回-1
int sendto(int sock, const char *buf, int len, int flags, const struct sockaddr* to, int tolen) // 成功返回发送的字节数,失败返回-1
前四个参数和recv一样,这里不做赘述了,recvfrom会从接受端套接字接收数据并且会得到发送方的套接字地址,而sendto会从发送端发送数据给需要接收的地址。
这两组函数对于TCP和UDP来说其实都是可用的,UDP也可以connect之后使用recv和send,(又给自己挖了个坑,以后有时间补上)。
接下来是TCP版完整程序,首先是TCP的接发数据的流程,如图
#include <winsock.h>
#pragma comment(lib,"WS2_32") // 链接到WS2_32.lib
#include <stdlib.h>
// 用来初始化winsock
class InitSock
{
public:
InitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0) // 返回0表示正常情况,否则退出程序
{
exit(0);
}
}
~InitSock()
{
::WSACleanup();
}
};
使用winsock时必须要初始化,这段代码将初始化封装在一个类里面,程序开始前创建一个对象就行了。(关于#pragma comment(lib,"WS2_32") 在这里留个坑,以后有空再说)
TCPServer.cpp
#include "InitSock.h"
#include <iostream>
#include <string>
InitSock sock;
int main() {
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = INADDR_ANY; // inet_addr("0.0.0.0");
// 绑定这个套接字到一个本地地址
if (::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR) {
std::cout << "Failed bind()" << std::endl;
return -1;
}
// 进入监听模式
if (::listen(sListen, 2) == SOCKET_ERROR)
{
printf("Failed listen() \n");
return -1;
}
// 循环接受客户的连接请求
sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr);
SOCKET sClient = 0;
char szText[] = " TCP Server Demo! \r\n";
while (sClient == 0)
{
// 接受一个新连接
sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen); // 会阻塞等待直到连接成功后返回一个套接字
if (sClient == INVALID_SOCKET)
{
printf("Failed accept()");
}
std::cout << "接收到一个连接:" << inet_ntoa(remoteAddr.sin_addr) << std::endl;
continue;
}
while (TRUE)
{
// 从客户端接收数据
char buff[256];
int nRecv = ::recv(sClient, buff, 256, 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
std::cout << "接收到的数据:" << buff << std::endl;
}
// 向客户端发送数据
std::cin >> szText;
::send(sClient, szText, strlen(szText), 0);
}
// 关闭同客户端的连接
::closesocket(sClient);
// 关闭监听套节字
::closesocket(sListen);
return 0;
}
TCPClient.cpp
#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock; // 初始化winsock 库
int main()
{
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字
if (s == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
sockaddr_in addr; // 远程地址
addr.sin_family = AF_INET;
// 填写的是服务器程序的ip地址
addr.sin_port = htons(4567); // 用来保存端口号
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
std::cout << "Failed connect()" << std::endl;
return -1;
}
char buff[256];
char text[256];
while (true) {
// 向服务端发送数据
std::cin >> text;
::send(s, text, strlen(text), 0);
int nrecv = ::recv(s, buff, 256, 0);
if (nrecv > 0) {
buff[nrecv] = '\0';
std::cout << "接收到数据:" << buff << std::endl;
}
}
::closesocket(s);
return 0;
}
接下来时UDP版的:
UDPServer.cpp
#include <iostream>
#include "InitSock.h"
InitSock sock;
int main() {
SOCKET sock_Server = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock_Server == INVALID_SOCKET)
{
std::cout << "Failed socket()" << std::endl;
return -1;
}
sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8888);
ser_addr.sin_addr.S_un.S_addr = INADDR_ANY; // "0.0.0.0"
// UDP不需要监听
if (::bind(sock_Server, (LPSOCKADDR)&ser_addr, sizeof(ser_addr)) == SOCKET_ERROR) {
std::cout << "Failed bind()" << std::endl;
return -1;
}
char sendmsg[256];
char recvmsg[256];
sockaddr_in cli_addr;
int len = sizeof(cli_addr);
while (TRUE) {
int count = recvfrom(sock_Server, recvmsg, 256, 0, (sockaddr*)&cli_addr, &len);
if (count == -1) {
std::cout << "Recive data fail!" << std::endl;
return -1;
}
std::cout << "收到的数据:" << recvmsg << std::endl; // 打印收到的数据
std::cin >> sendmsg;
sendto(sock_Server, sendmsg, 256, 0, (sockaddr*)&cli_addr, len);
}
::closesocket(sock_Server);
return 0;
}
UDPClient.cpp
#include <iostream>
#include "InitSock.h"
#include <string>
InitSock sock; // 初始化winsock 库
int main()
{
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字
if (s == INVALID_SOCKET) {
std::cout << "Failed socket()" << std::endl;
return -1;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
sockaddr_in addr; // 远程地址
addr.sin_family = AF_INET;
// 填写的是服务器程序的ip地址
addr.sin_port = htons(4567); // 用来保存端口号
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
if (::connect(s, (sockaddr*)&addr, sizeof(addr)) == -1) {
std::cout << "Failed connect()" << std::endl;
return -1;
}
char buff[256];
char text[256];
while (true) {
// 向服务端发送数据
std::cin >> text;
::send(s, text, strlen(text), 0);
int nrecv = ::recv(s, buff, 256, 0);
if (nrecv > 0) {
buff[nrecv] = '\0';
std::cout << "接收到数据:" << buff << std::endl;
}
}
::closesocket(s);
return 0;
}