最近搭建了一个Linux服务器做FTP网盘,突发奇想写写Linux程序(不然岂不浪费了一台电脑???),因为是Linux程序,肯定是作为服务器端运行啦,服务器程序怎么离得开Socket咧?所以呢,先在Linux下写个简单的ECHO小试鸡刀(/捂脸)。
其实Linux Socket编程跟Windows差不多,最底层还是socket、bind、listen、accept/connect这些函数。不一样的大概就四个地方:
1、头文件——在Windows下,所有Socket函数都包含在WinSock.h下,而在Linux下就不一样了,socket函数在<sys/types.h>和<sys/socket.h>下,close函数则在<unistd.h>下。不同的函数可能包含在众多不同的头文件下(实在是记不住啊,还好Linux下有man,不然真得撞墙了)。
2、动态链接库——众所周知在Windows下写网络程序,都需要链接Ws2_32.dll。而在Linux下不需要。
3、关闭socket对象函数——在Windows下SOCKET对象用完之后要调用closesocket将其释放掉,而在Linux下同样也需要释放,不同的是调用的函数不一样,Linux下调用close函数。
4、套接字结构体——在Windows下,socket返回的是SOCKET,而在Linux下,socket返回的是int。其实查看Windows头文件可以看到SOCKET这个类型其实就是转定义类型。其本质上是int。
服务端/客户端代码流程与Windows是一致的:
服务器端程序基本流程:创建服务端套接字-->绑定套接字-->监听套接字-->接收客户端连接请求-->收/发消息-->释放客户端套接字对象-->释放服务端套接字对象。
客户端程序基本流程:创建服务端套接字-->连接服务端-->收/发消息-->释放套接字对象。
以下为程序代码
// Server.cpp
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <iostream> #include <string.h> #include <string> #include <unistd.h> int main() { try{ // 创建服务端套接字 int so = socket(AF_INET, SOCK_STREAM, 0); if(so == -1) throw "Create socket error."; // 绑定服务端套接字到指定的IP与Port上 sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(9089); addr.sin_addr.s_addr = INADDR_ANY; if(bind(so, (sockaddr*)&addr, sizeof(sockaddr_in)) == -1) throw "绑定套截字失败"; // 监听套接字 if(listen(so, 5) == -1) throw "监听套接字失败"; std::cout << "Start server finish ..." << std::endl; // 客户端信息 int soClient; int nClientAddSize; sockaddr_in addrClient; while(true) { // 初始化客户端信息存储地址 soClient = 0; nClientAddSize = sizeof(sockaddr_in); memset(&addrClient, 0, sizeof(sockaddr_in)); // 接收客户端的连接请求 soClient = accept(so, (sockaddr*)&addrClient, (socklen_t*)&nClientAddSize); if(soClient == -1)continue; // 接收客户端发送过来的数据 const unsigned short cunRecvArraySize = 1024; char szRecv[cunRecvArraySize]; memset(szRecv, 0, sizeof(char) * cunRecvArraySize); int nRecvSize = recv(soClient, szRecv, cunRecvArraySize * sizeof(char), 0); std::cout << szRecv << std::endl; // 发送数据给客户端 if(send(soClient, szRecv, nRecvSize, 0) != nRecvSize) std::cout << "Send message to client error." << std::endl; // 关闭客户端的连接,释放客户端套接字对象 close(soClient); } // 释放服务端的套接字对象 close(so); }catch(const char *pMsg) { std::cout << pMsg << std::endl; } return 0; }
// Client.cpp
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <string.h> int main() { try{ int so = socket(AF_INET, SOCK_STREAM, 0); if(so == -1)throw "Create socket error."; sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_port = htons(9089); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(connect(so, (sockaddr*)&addr, sizeof(addr)) == -1)throw "Connect server error."; std::cout << "Send:" << "Hello" << std::endl; if(send(so, "Hello", strlen("Hello"), 0) != strlen("Hello"))throw "Send message to server error."; char szRecv[512]; memset(szRecv, 0, 512); int nRecv = recv(so, szRecv, 512, 0); if(nRecv < 1)throw "Recv message to server error."; std::cout << "Recv:" << szRecv << std::endl; close(so); }catch(const char *pMsg) { std::cout << pMsg << std::endl; } return 0; }
随便写写,不然漫漫长夜怎么过(/捂脸)
顺便再解释下各个函数的参数及返回值吧!
int socket( int af, int type, int protocol);
af:一个地址描述。目前仅支持AF_INET格式
type:套接字的类型。常用的为流式套接字和数据报套接字。流式套接字即为TCP协议,面向连接的可靠的;数据报呢则为UDP协议,无连接不可靠的。
protocol:套接字使用的协议。常用的协议有IPPROTO_TCP和IPPROTO_UDP等、当然也可以不显式指定使用何种协议,如果不显式指定则传入0即可。
返回值:没有错误发生的情况,返回一个新的套接字。如果发生错误,则返回-1。
示例:int so = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
sockfd:被绑定的套接字。即我们上面调用socket function返回的套接字对象。
my_addr: 这是一个结构体,这个结构体存储了套接字所使用的端口号、地址及协议。后面再详细讲这个结构体
addrlen:就是上面这个my_addr结构体的大小。
返回值:没有错误发生返回0,否则返回-1。
示例:
sockaddr_in addr;
// TODO:给addr赋值
bind(so, (sockaddr*)&addr, sizeof(addr);
int listen(int fd, int backlog);
fd:被监听的套接字。即我们上面调用socket function返回的套接字对象。
backlog:连接请求队列最大长度。一般传5即可。
返回值:没有错误发生返回0,否则返回-1。
示例:listen(so, 5);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:从哪个套接字接收客户端的连接,一般为我们上面调用socket function返回的套接字对象。
addr:客户端地址信息。
addrlen:客户端地址信息的大小。注意这里传入的是指针,且指针的值不能为0.
返回值:没有错误发生的情况,返回一个新的客户端套接字。后续与客户端通信就用这个套接字对象。如果发生错误,则返回-1。
示例:sockaddr_in addrClient; int nClientAddrSize = sizeof(sockaddr_in);
int soClient = accept(so, (sockaddr*)&addr, &nClientAddrSize);
int connect(int s, const struct sockaddr * name, int namelen);
s:需要连接服务端的套接字对象。即我们上面调用socket function返回的套接字对象。
name:服务端的地址信息
namelen:服务端地址信息的大小
返回值:没有错误发生返回0,否则返回-1。
示例:connect(so, (sockaddr*)&addr, sizeof(sockaddr_in));
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:接收数据的套接字。你要给某人发短信,那你总得告诉运营商你要给谁发短信吧。
buf:要发送的信息。
len:信息长度。
flags:这个好像是优先级。我们一般传入0即可。
返回值:没有错误发生返回实际发送的大小,否则返回-1.
示例:send(so, "hello", strlen("hello"), 0);
int recv(int s, void* buf, int len, int flags)
s:要接收信息的套接字。
buf:接收到的信息存放的缓存区
len:缓存区大小
flags:这个好像是优先级。我们一般传入0即可。
返回值:没有错误发生返回实际接收的大小,否则返回-1 or 0。
示例:int nRecv = recv(soClient, pRecvBuffer, cunRecvBufferSize, 0);
int close(int fd)
fd:要关闭的套接字对象
返回值:没有错误发生返回0,否则返回-1。
示例:close(soClient);
函数就是以上这些了。下面我们来聊聊sockaddr_in结构体
sockaddr_in 定义如下:
struct
sockaddr_in
{
short
sin_family;
unsigned
short
sin_port;
/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/
struct
in_addr sin_addr;
/*IP address in network byte order(Internet address)*/
unsigned
char
sin_zero[8];
/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/
};
sin_family:一个地址描述。必须与socket function第一个参数相同。
sin_port:端口号。要注意的是这个端口号使用的是网络字节顺序,而不是我们通常使用的主机字节顺序。所有设定这个值得时候需要转换。转换函数有htons、htonl主机字节转网络字节,htons转换成整型网络字节顺序,htonl转换成无符号长整型的网络字节顺序。一般我们使用htons即可。
sin_addr:地址。与端口号一致,都是使用的网络字节顺序。可以使用inet_addr function将点分十进制的IP地址转换成长整型网络字节顺序。相反网络字节顺序转点分十进制的function是 inet_ntoa 。如果指定固定地址的话,可以传入INADDR_ANY,表示所有地址。
sin_zero:不需要。
示例:
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;